clankerbend 0.1.1 → 0.1.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/README.md +16 -2
- package/apps/sticky-notes/public/app.js +35 -2
- package/apps/vim-nav/public/app.js +35 -2
- package/cli.mjs +7 -4
- package/docs/launcher-profiles.md +32 -0
- package/docs/protocol.md +21 -10
- package/host/src/app-registry.js +2 -0
- package/host/src/codex-desktop-cdp-adapter.js +48 -6
- package/host/src/codex-desktop-renderer-bridge.js +636 -1
- package/host/src/index.js +156 -4
- package/launch/accounts.mjs +360 -0
- package/launch/codex-mux-adapter.mjs +175 -0
- package/launch/profiles.mjs +30 -6
- package/launch/runtime-paths.mjs +3 -0
- package/launch/test-accounts.mjs +71 -0
- package/package.json +1 -1
- package/server.mjs +14 -7
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ ClankerBend lets you build Codex Desktop plugins without rebuilding or re-signin
|
|
|
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
|
+
[YouTube demo](https://www.youtube.com/watch?v=FcQUxiL6enc) (or click on the screenshot):
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
13
14
|
<a href="https://www.youtube.com/watch?v=FcQUxiL6enc">
|
|
@@ -30,6 +31,7 @@ npx clankerbend
|
|
|
30
31
|
|
|
31
32
|
That starts Codex Desktop with the default ClankerBend profile and bundled apps:
|
|
32
33
|
|
|
34
|
+
- **ClankerID**: switch between Codex accounts.
|
|
33
35
|
- **VimNav**: Vim-style transcript navigation, number rails, role jumps, search,
|
|
34
36
|
and a side panel.
|
|
35
37
|
- **Sticky Notes**: select transcript text, click **Add note**, and the
|
|
@@ -45,9 +47,21 @@ ClankerBend writes runtime state outside the package:
|
|
|
45
47
|
|
|
46
48
|
Override with `ONEWILL_CLANKERBEND_STATE_DIR`.
|
|
47
49
|
|
|
50
|
+
Managed Codex account profiles live under the same state directory. The primary
|
|
51
|
+
profile remains your normal `CODEX_HOME` or `~/.codex`; managed profiles get
|
|
52
|
+
their own `codex-home` and Electron user-data directories. You can start a
|
|
53
|
+
saved profile directly with:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx clankerbend --account work
|
|
57
|
+
```
|
|
58
|
+
|
|
48
59
|
## Security
|
|
49
60
|
|
|
50
61
|
ClankerBend binds to `127.0.0.1` on an OS-assigned ephemeral port. The launcher
|
|
51
|
-
prints the exact host URL, for example
|
|
62
|
+
prints the exact host URL and bearer token, for example
|
|
63
|
+
`Host: http://127.0.0.1:49152` and `Token: ...`.
|
|
52
64
|
`/clankerbend/*` endpoints require a bearer token by default. For local protocol
|
|
53
|
-
debugging, you can set `ONEWILL_CLANKERBEND_DISABLE_AUTH=1`.
|
|
65
|
+
debugging, you can set `ONEWILL_CLANKERBEND_DISABLE_AUTH=1`. Hosted app pages
|
|
66
|
+
receive the same bearer token from their app URL fragment and from a
|
|
67
|
+
host-injected HTML bootstrap so URL rewriting does not break app auth.
|
|
@@ -57,11 +57,44 @@ async function runAction(type, payload = {}) {
|
|
|
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,
|
|
@@ -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,
|
package/cli.mjs
CHANGED
|
@@ -54,6 +54,8 @@ function parseCodexArgs(args) {
|
|
|
54
54
|
options.mock = true;
|
|
55
55
|
} else if (arg === "--state-dir") {
|
|
56
56
|
options.stateDir = requiredArg(args[++index], "--state-dir value");
|
|
57
|
+
} else if (arg === "--account") {
|
|
58
|
+
options.accountId = requiredArg(args[++index], "--account id");
|
|
57
59
|
} else if (arg === "--help" || arg === "-h") {
|
|
58
60
|
printCodexHelp();
|
|
59
61
|
process.exit(0);
|
|
@@ -68,8 +70,8 @@ function printHelp() {
|
|
|
68
70
|
console.log(`ClankerBend
|
|
69
71
|
|
|
70
72
|
Usage:
|
|
71
|
-
clankerbend [--mock] [--state-dir path]
|
|
72
|
-
clankerbend codex [--mock] [--state-dir path]
|
|
73
|
+
clankerbend [--mock] [--state-dir path] [--account id]
|
|
74
|
+
clankerbend codex [--mock] [--state-dir path] [--account id]
|
|
73
75
|
clankerbend help
|
|
74
76
|
clankerbend --version
|
|
75
77
|
|
|
@@ -84,12 +86,13 @@ function printCodexHelp() {
|
|
|
84
86
|
console.log(`ClankerBend Codex launcher
|
|
85
87
|
|
|
86
88
|
Usage:
|
|
87
|
-
clankerbend [--mock] [--state-dir path]
|
|
88
|
-
clankerbend codex [--mock] [--state-dir path]
|
|
89
|
+
clankerbend [--mock] [--state-dir path] [--account id]
|
|
90
|
+
clankerbend codex [--mock] [--state-dir path] [--account id]
|
|
89
91
|
|
|
90
92
|
Options:
|
|
91
93
|
--mock Start against a mock transcript instead of Codex Desktop.
|
|
92
94
|
--state-dir path Override ClankerBend runtime state directory.
|
|
95
|
+
--account id Start with a saved ClankerBend Codex account profile.
|
|
93
96
|
`);
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -46,6 +46,38 @@ Runtime state defaults to:
|
|
|
46
46
|
|
|
47
47
|
Set `ONEWILL_CLANKERBEND_STATE_DIR` to override the root state directory.
|
|
48
48
|
|
|
49
|
+
## Codex Account Profiles
|
|
50
|
+
|
|
51
|
+
ClankerBend can remember up to 20 Codex account profiles. It still launches at
|
|
52
|
+
most one Codex Desktop instance at a time. Switching accounts stops the current
|
|
53
|
+
ClankerBend-launched Desktop process and starts the selected profile.
|
|
54
|
+
|
|
55
|
+
The `primary` profile always points at the normal Codex home:
|
|
56
|
+
|
|
57
|
+
- `CODEX_HOME` when set
|
|
58
|
+
- otherwise `~/.codex`
|
|
59
|
+
|
|
60
|
+
Managed profiles are created empty under the ClankerBend state directory. Each
|
|
61
|
+
managed profile gets its own `codex-home` and Electron user-data directory. No
|
|
62
|
+
auth, sessions, skills, plugins, or history are copied from the primary profile.
|
|
63
|
+
If the primary profile is file-auth based, the managed profile receives only a
|
|
64
|
+
minimal `config.toml` auth-store setting so Codex can use the same auth mode.
|
|
65
|
+
|
|
66
|
+
ClankerID exposes these operations:
|
|
67
|
+
|
|
68
|
+
- **Switch**: stop the current Desktop process and launch the selected profile.
|
|
69
|
+
- **Launch by default**: choose which profile ClankerBend starts next time. This
|
|
70
|
+
does not change the primary Codex home.
|
|
71
|
+
- **Make primary**: promote a managed profile into the primary Codex home.
|
|
72
|
+
ClankerBend first moves the old primary home to
|
|
73
|
+
`<primary>.backup_<timestamp>` (normally `~/.codex.backup_<timestamp>`),
|
|
74
|
+
preserves it as a managed account, then moves the selected managed home into
|
|
75
|
+
the primary Codex home.
|
|
76
|
+
- **Rollback**: restore a previous primary backup and move the replaced primary
|
|
77
|
+
to `<primary>.rollback_replaced_<timestamp>`.
|
|
78
|
+
- **Archive**: remove a managed profile from the active account list and move
|
|
79
|
+
its directories into deleted-account storage.
|
|
80
|
+
|
|
49
81
|
## Ports And Cleanup
|
|
50
82
|
|
|
51
83
|
The host and CDP adapter bind to `127.0.0.1` and use ephemeral ports. Launchers
|
package/docs/protocol.md
CHANGED
|
@@ -239,17 +239,20 @@ An app must not bypass the host token by calling CDP or app-server directly.
|
|
|
239
239
|
ClankerBend `0.1` defines two token bootstrap paths:
|
|
240
240
|
|
|
241
241
|
1. **Hosted panel apps**: the host serves the app entry URL and may append the
|
|
242
|
-
token in the URL fragment as `#clankerbend_token=<token>`.
|
|
243
|
-
the
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
token in the URL fragment as `#clankerbend_token=<token>`. For served HTML,
|
|
243
|
+
the host may also inject a token bootstrap such as
|
|
244
|
+
`window.__CLANKERBEND_TOKEN` or `<meta name="clankerbend-token">` so URL
|
|
245
|
+
rewriting does not strand the app without a token. App JavaScript stores the
|
|
246
|
+
token in memory, may keep it in `sessionStorage` for hash-less reloads,
|
|
247
|
+
immediately removes any fragment with `history.replaceState`, and sends the
|
|
248
|
+
token in the `Authorization` header for JSON and SSE requests.
|
|
246
249
|
2. **External clients**: the token is provided out of band by the launcher, such
|
|
247
|
-
as an environment variable, config file, local IPC
|
|
250
|
+
as the `Token:` startup line, an environment variable, config file, local IPC
|
|
251
|
+
channel, or command output.
|
|
248
252
|
|
|
249
253
|
Hosts must not require a token in the query string. Query strings are more
|
|
250
|
-
likely to be logged or copied than URL fragments.
|
|
251
|
-
|
|
252
|
-
support the bearer-token header.
|
|
254
|
+
likely to be logged or copied than URL fragments. Clients must support the
|
|
255
|
+
bearer-token header.
|
|
253
256
|
|
|
254
257
|
## State Model
|
|
255
258
|
|
|
@@ -752,13 +755,21 @@ In either model:
|
|
|
752
755
|
ClankerBend can launch Codex Desktop with CDP:
|
|
753
756
|
|
|
754
757
|
```sh
|
|
755
|
-
/Applications/
|
|
758
|
+
/Applications/ChatGPT.app/Contents/MacOS/ChatGPT \
|
|
756
759
|
--remote-debugging-address=127.0.0.1 \
|
|
757
760
|
--remote-debugging-port=<free> \
|
|
758
761
|
--user-data-dir=<test-or-clankerbend-profile>
|
|
759
762
|
```
|
|
760
763
|
|
|
761
|
-
|
|
764
|
+
Current Desktop releases use the `ChatGPT.app` path while retaining the Codex
|
|
765
|
+
bundle identity and bundled `Resources/codex` CLI. ClankerBend discovers that
|
|
766
|
+
layout and the legacy `/Applications/Codex.app` layout automatically.
|
|
767
|
+
|
|
768
|
+
When account profiles are enabled, the adapter also launches Desktop with the
|
|
769
|
+
selected profile's `CODEX_HOME` and isolated Electron user-data directory.
|
|
770
|
+
ClankerBend `0.1` runs one managed Codex Desktop instance at a time.
|
|
771
|
+
|
|
772
|
+
The host must not patch either application bundle.
|
|
762
773
|
|
|
763
774
|
### Attach
|
|
764
775
|
|
package/host/src/app-registry.js
CHANGED
|
@@ -144,6 +144,8 @@ export async function loadProfileFromManifests(options) {
|
|
|
144
144
|
hostId: options.hostId,
|
|
145
145
|
hostName: options.hostName,
|
|
146
146
|
runDir: options.runDir,
|
|
147
|
+
runtimePaths: options.runtimePaths,
|
|
148
|
+
accountRegistry: options.accountRegistry,
|
|
147
149
|
defaultPanelAppId,
|
|
148
150
|
apps,
|
|
149
151
|
manifests: loadedApps.map((loaded) => loaded.manifest),
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import net from "node:net";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const CODEX_DESKTOP_EXECUTABLES = [
|
|
7
|
+
"/Applications/Codex.app/Contents/MacOS/Codex",
|
|
8
|
+
"/Applications/ChatGPT.app/Contents/MacOS/ChatGPT"
|
|
9
|
+
];
|
|
8
10
|
|
|
9
11
|
export function createCodexDesktopCdpAdapter(options = {}) {
|
|
10
12
|
return new CodexDesktopCdpAdapter(options);
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export function resolveCodexDesktopPaths(options = {}) {
|
|
16
|
+
const codexApp = options.codexApp || CODEX_DESKTOP_EXECUTABLES.find((candidate) => existsSync(candidate)) || CODEX_DESKTOP_EXECUTABLES[0];
|
|
17
|
+
const bundledCli = join(dirname(dirname(codexApp)), "Resources/codex");
|
|
18
|
+
const codexCli = options.codexCli || (existsSync(bundledCli)
|
|
19
|
+
? bundledCli
|
|
20
|
+
: CODEX_DESKTOP_EXECUTABLES
|
|
21
|
+
.map((candidate) => join(dirname(dirname(candidate)), "Resources/codex"))
|
|
22
|
+
.find((candidate) => existsSync(candidate))) || bundledCli;
|
|
23
|
+
return { codexApp, codexCli };
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
function normalizeRendererBridges(options = {}) {
|
|
14
27
|
const bridges = options.rendererBridges?.length
|
|
15
28
|
? options.rendererBridges
|
|
@@ -63,10 +76,13 @@ class CodexDesktopCdpAdapter {
|
|
|
63
76
|
this.cdp = true;
|
|
64
77
|
this.rendererInjection = true;
|
|
65
78
|
this.rendererFetchToLoopback = false;
|
|
66
|
-
|
|
67
|
-
this.
|
|
79
|
+
const desktopPaths = resolveCodexDesktopPaths(options);
|
|
80
|
+
this.codexApp = desktopPaths.codexApp;
|
|
81
|
+
this.codexCli = desktopPaths.codexCli;
|
|
68
82
|
this.runDir = options.runDir ? resolve(options.runDir) : null;
|
|
69
83
|
this.profileDir = options.profileDir || (this.runDir ? join(this.runDir, "codex-profile") : null);
|
|
84
|
+
this.codexHome = options.codexHome ? resolve(options.codexHome) : null;
|
|
85
|
+
this.resetProfileDir = options.resetProfileDir === true;
|
|
70
86
|
this.rendererBridges = normalizeRendererBridges(options);
|
|
71
87
|
this.providers = normalizeProviders(options, this.rendererBridges);
|
|
72
88
|
this.snapshotToTranscript = options.snapshotToTranscript || defaultSnapshotToTranscript;
|
|
@@ -86,6 +102,12 @@ class CodexDesktopCdpAdapter {
|
|
|
86
102
|
|
|
87
103
|
async start(host) {
|
|
88
104
|
this.host = host;
|
|
105
|
+
if (!existsSync(this.codexApp)) {
|
|
106
|
+
throw new Error(`Codex Desktop executable not found. Looked for: ${CODEX_DESKTOP_EXECUTABLES.join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
if (!existsSync(this.codexCli)) {
|
|
109
|
+
throw new Error(`Codex CLI not found in the Codex Desktop bundle: ${this.codexCli}`);
|
|
110
|
+
}
|
|
89
111
|
if (!this.rendererBridges.length) throw new Error("at least one renderer bridge is required");
|
|
90
112
|
for (const bridge of this.rendererBridges) {
|
|
91
113
|
if (!bridge.injectedScriptPath) throw new Error(`injectedScriptPath is required for ${bridge.appId || "renderer bridge"}`);
|
|
@@ -93,9 +115,10 @@ class CodexDesktopCdpAdapter {
|
|
|
93
115
|
}
|
|
94
116
|
if (this.runDir) mkdirSync(this.runDir, { recursive: true });
|
|
95
117
|
if (this.profileDir) {
|
|
96
|
-
rmSync(this.profileDir, { recursive: true, force: true });
|
|
118
|
+
if (this.resetProfileDir) rmSync(this.profileDir, { recursive: true, force: true });
|
|
97
119
|
mkdirSync(this.profileDir, { recursive: true });
|
|
98
120
|
}
|
|
121
|
+
if (this.codexHome) mkdirSync(this.codexHome, { recursive: true });
|
|
99
122
|
|
|
100
123
|
this.cdpPort = await freePort();
|
|
101
124
|
this.child = spawn(this.codexApp, [
|
|
@@ -105,6 +128,7 @@ class CodexDesktopCdpAdapter {
|
|
|
105
128
|
], {
|
|
106
129
|
env: {
|
|
107
130
|
...process.env,
|
|
131
|
+
...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}),
|
|
108
132
|
...(this.profileDir ? { CODEX_ELECTRON_USER_DATA_PATH: this.profileDir } : {})
|
|
109
133
|
},
|
|
110
134
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -885,6 +909,24 @@ class CodexDesktopCdpAdapter {
|
|
|
885
909
|
await this.host.highlightRange(event.range, { durationMs: 1200 });
|
|
886
910
|
} else if (event.kind === "highlightAnchor" && event.anchorId) {
|
|
887
911
|
await this.host.highlightAnchor(event.anchorId, { durationMs: 1200 });
|
|
912
|
+
} else if (event.kind === "codexAccountSwitch" && event.accountId) {
|
|
913
|
+
if (typeof this.host.transcriptAdapter.switchTo !== "function") throw new Error("Codex account switching is unavailable");
|
|
914
|
+
await this.host.transcriptAdapter.switchTo(String(event.accountId));
|
|
915
|
+
} else if (event.kind === "codexAccountCreateAndSwitch") {
|
|
916
|
+
if (typeof this.host.transcriptAdapter.createAccount !== "function" || typeof this.host.transcriptAdapter.switchTo !== "function") {
|
|
917
|
+
throw new Error("Codex account creation is unavailable");
|
|
918
|
+
}
|
|
919
|
+
const account = await this.host.transcriptAdapter.createAccount({ label: String(event.label || "Account") });
|
|
920
|
+
await this.host.transcriptAdapter.switchTo(account.id);
|
|
921
|
+
} else if (event.kind === "codexAccountSetDefault" && event.accountId) {
|
|
922
|
+
if (typeof this.host.transcriptAdapter.setDefault !== "function") throw new Error("Codex account defaults are unavailable");
|
|
923
|
+
await this.host.transcriptAdapter.setDefault(String(event.accountId));
|
|
924
|
+
} else if (event.kind === "codexAccountAdoptAsPrimary" && event.accountId) {
|
|
925
|
+
if (typeof this.host.transcriptAdapter.adoptAsPrimary !== "function") throw new Error("Codex primary adoption is unavailable");
|
|
926
|
+
await this.host.transcriptAdapter.adoptAsPrimary(String(event.accountId));
|
|
927
|
+
} else if (event.kind === "codexAccountDelete" && event.accountId) {
|
|
928
|
+
if (typeof this.host.transcriptAdapter.deleteAccount !== "function") throw new Error("Codex account removal is unavailable");
|
|
929
|
+
await this.host.transcriptAdapter.deleteAccount(String(event.accountId));
|
|
888
930
|
}
|
|
889
931
|
}
|
|
890
932
|
}
|