clankerbend 0.1.1 → 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/README.md +15 -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 +15 -8
- package/host/src/app-registry.js +2 -0
- package/host/src/codex-desktop-cdp-adapter.js +23 -1
- package/host/src/codex-desktop-renderer-bridge.js +633 -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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createCodexDesktopCdpAdapter } from "../host/src/codex-desktop-cdp-adapter.js";
|
|
2
|
+
import { PRIMARY_ACCOUNT_ID } from "./accounts.mjs";
|
|
3
|
+
|
|
4
|
+
export function createCodexDesktopMuxAdapter(options = {}) {
|
|
5
|
+
return new CodexDesktopMuxAdapter(options);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class CodexDesktopMuxAdapter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
if (!options.accountRegistry) throw new Error("accountRegistry is required");
|
|
11
|
+
this.name = "codex-desktop-mux";
|
|
12
|
+
this.cdp = true;
|
|
13
|
+
this.rendererInjection = true;
|
|
14
|
+
this.rendererFetchToLoopback = false;
|
|
15
|
+
this.accountRegistry = options.accountRegistry;
|
|
16
|
+
this.adapterOptions = options.adapterOptions || {};
|
|
17
|
+
this.providers = options.providers || undefined;
|
|
18
|
+
this.startAccountId = options.startAccountId || null;
|
|
19
|
+
this.host = null;
|
|
20
|
+
this.activeAccountId = null;
|
|
21
|
+
this.activeAdapter = null;
|
|
22
|
+
this.switching = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start(host) {
|
|
26
|
+
this.host = host;
|
|
27
|
+
const account = this.startAccountId ? this.accountRegistry.get(this.startAccountId) : this.accountRegistry.getDefault();
|
|
28
|
+
await this.switchTo(account.id, { initial: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async stop() {
|
|
32
|
+
await this.stopActive();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async switchTo(accountId, options = {}) {
|
|
36
|
+
if (this.switching) throw new Error("Codex account switch already in progress");
|
|
37
|
+
this.switching = true;
|
|
38
|
+
try {
|
|
39
|
+
const account = this.accountRegistry.get(accountId);
|
|
40
|
+
if (!options.initial) await this.stopActive();
|
|
41
|
+
this.activeAccountId = account.id;
|
|
42
|
+
this.host.state.codexAccounts = this.accountState();
|
|
43
|
+
this.host.setDesktopStatus({
|
|
44
|
+
cdpStatus: "starting",
|
|
45
|
+
cdpPort: null,
|
|
46
|
+
desktopPid: null,
|
|
47
|
+
target: null,
|
|
48
|
+
error: null
|
|
49
|
+
});
|
|
50
|
+
this.host.updateTranscript({
|
|
51
|
+
anchors: [],
|
|
52
|
+
visibleCount: 0,
|
|
53
|
+
annotationCount: 0,
|
|
54
|
+
scroll: null,
|
|
55
|
+
updatedAt: new Date().toISOString()
|
|
56
|
+
}, { broadcast: false });
|
|
57
|
+
this.activeAdapter = createCodexDesktopCdpAdapter({
|
|
58
|
+
...this.adapterOptions,
|
|
59
|
+
profileDir: account.electronProfile,
|
|
60
|
+
codexHome: account.codexHome,
|
|
61
|
+
resetProfileDir: false
|
|
62
|
+
});
|
|
63
|
+
await this.activeAdapter.start(this.host);
|
|
64
|
+
this.providers = this.activeAdapter.providers;
|
|
65
|
+
return {
|
|
66
|
+
switched: true,
|
|
67
|
+
activeAccountId: account.id,
|
|
68
|
+
account: this.accountRegistry.publicAccount(account)
|
|
69
|
+
};
|
|
70
|
+
} finally {
|
|
71
|
+
this.switching = false;
|
|
72
|
+
if (this.host) {
|
|
73
|
+
this.host.state.codexAccounts = this.accountState();
|
|
74
|
+
this.host.touchAndBroadcast();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stopActive() {
|
|
80
|
+
const adapter = this.activeAdapter;
|
|
81
|
+
this.activeAdapter = null;
|
|
82
|
+
if (adapter) await adapter.stop?.();
|
|
83
|
+
if (this.host) {
|
|
84
|
+
this.host.setDesktopStatus({
|
|
85
|
+
cdpStatus: "exited",
|
|
86
|
+
cdpPort: null,
|
|
87
|
+
desktopPid: null,
|
|
88
|
+
target: null,
|
|
89
|
+
error: null
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
accountState() {
|
|
95
|
+
return {
|
|
96
|
+
available: true,
|
|
97
|
+
activeAccountId: this.activeAccountId,
|
|
98
|
+
maxRunningDesktopInstances: 1,
|
|
99
|
+
switching: this.switching,
|
|
100
|
+
...this.accountRegistry.list()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async createAccount(input = {}) {
|
|
105
|
+
const account = this.accountRegistry.createManaged(input);
|
|
106
|
+
this.host.state.codexAccounts = this.accountState();
|
|
107
|
+
this.host.touchAndBroadcast();
|
|
108
|
+
return account;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async setDefault(accountId) {
|
|
112
|
+
const result = this.accountRegistry.setDefault(accountId);
|
|
113
|
+
this.host.state.codexAccounts = this.accountState();
|
|
114
|
+
this.host.touchAndBroadcast();
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async adoptAsPrimary(accountId) {
|
|
119
|
+
await this.stopActive();
|
|
120
|
+
const result = this.accountRegistry.adoptAsPrimary(accountId);
|
|
121
|
+
const launched = await this.switchTo(PRIMARY_ACCOUNT_ID);
|
|
122
|
+
return { ...result, launched };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async rollbackPrimary(input = {}) {
|
|
126
|
+
await this.stopActive();
|
|
127
|
+
const result = this.accountRegistry.rollbackPrimary(input);
|
|
128
|
+
const launched = await this.switchTo(PRIMARY_ACCOUNT_ID);
|
|
129
|
+
return { ...result, launched };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async deleteAccount(accountId) {
|
|
133
|
+
const account = this.accountRegistry.get(accountId);
|
|
134
|
+
if (account.kind === "primary") throw new Error("primary account cannot be deleted");
|
|
135
|
+
if (accountId === this.activeAccountId) await this.stopActive();
|
|
136
|
+
const result = this.accountRegistry.deleteManaged(accountId);
|
|
137
|
+
if (accountId === this.activeAccountId) this.activeAccountId = null;
|
|
138
|
+
this.host.state.codexAccounts = this.accountState();
|
|
139
|
+
this.host.touchAndBroadcast();
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
active() {
|
|
144
|
+
if (!this.activeAdapter) throw new Error("Codex Desktop is not running");
|
|
145
|
+
return this.activeAdapter;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
openPanel(...args) {
|
|
149
|
+
return this.active().openPanel(...args);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
scrollToAnchor(...args) {
|
|
153
|
+
return this.active().scrollToAnchor(...args);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
highlightAnchor(...args) {
|
|
157
|
+
return this.active().highlightAnchor(...args);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
highlightRange(...args) {
|
|
161
|
+
return this.active().highlightRange(...args);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setComposerDraft(...args) {
|
|
165
|
+
return this.active().setComposerDraft(...args);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
submitComposer(...args) {
|
|
169
|
+
return this.active().submitComposer(...args);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
attachFiles(...args) {
|
|
173
|
+
return this.active().attachFiles(...args);
|
|
174
|
+
}
|
|
175
|
+
}
|
package/launch/profiles.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
providersFromRendererBridges,
|
|
9
9
|
readAppManifest
|
|
10
10
|
} from "../host/src/app-registry.js";
|
|
11
|
+
import { CodexAccountRegistry, primaryCodexHome } from "./accounts.mjs";
|
|
11
12
|
import { clankerbendRuntimePaths } from "./runtime-paths.mjs";
|
|
12
13
|
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -23,6 +24,18 @@ export const STICKY_NOTES_APP_ID = readAppManifest(STICKY_NOTES_MANIFEST_PATH).a
|
|
|
23
24
|
export async function navigateProfile(options = {}) {
|
|
24
25
|
const runtimePaths = clankerbendRuntimePaths({ stateDir: options.stateDir });
|
|
25
26
|
const runDir = options.runDir || runtimePaths.runDir;
|
|
27
|
+
const registryProfileId = options.registryProfileId || process.env.ONEWILL_CLANKERBEND_PROFILE || "default";
|
|
28
|
+
const primaryElectronProfile = options.runDir ? join(runDir, "codex-profile") : runtimePaths.codexProfileDir;
|
|
29
|
+
const accountStateRoot = options.runDir && !options.stateDir ? runDir : runtimePaths.root;
|
|
30
|
+
const accountRegistry = new CodexAccountRegistry({
|
|
31
|
+
root: accountStateRoot,
|
|
32
|
+
registryPath: join(accountStateRoot, "accounts.json"),
|
|
33
|
+
accountsDir: join(accountStateRoot, "accounts"),
|
|
34
|
+
deletedDir: join(accountStateRoot, "deleted-accounts"),
|
|
35
|
+
primaryCodexHome: options.primaryCodexHome || primaryCodexHome(),
|
|
36
|
+
primaryElectronProfile
|
|
37
|
+
});
|
|
38
|
+
const codexAccount = options.accountId ? accountRegistry.get(options.accountId) : accountRegistry.getDefault();
|
|
26
39
|
const profile = await loadProfileFromManifests({
|
|
27
40
|
profileId: PROFILE_NAVIGATE,
|
|
28
41
|
name: "Navigate",
|
|
@@ -31,12 +44,18 @@ export async function navigateProfile(options = {}) {
|
|
|
31
44
|
hostName: "ClankerBend Navigate",
|
|
32
45
|
runDir,
|
|
33
46
|
runtimePaths,
|
|
47
|
+
accountRegistry,
|
|
34
48
|
defaultPanelAppId: VIM_NAV_APP_ID,
|
|
35
49
|
manifestPaths: configuredManifestPaths([VIM_NAV_MANIFEST_PATH, STICKY_NOTES_MANIFEST_PATH], {
|
|
36
50
|
registryConfigPath: options.registryConfigPath || runtimePaths.registryConfigPath,
|
|
37
|
-
registryProfileId
|
|
51
|
+
registryProfileId
|
|
38
52
|
})
|
|
39
53
|
});
|
|
54
|
+
profile.launchProfileId = registryProfileId;
|
|
55
|
+
profile.launchProfileName = profileNameFromId(registryProfileId);
|
|
56
|
+
profile.accountRegistry = accountRegistry;
|
|
57
|
+
profile.codexAccount = codexAccount;
|
|
58
|
+
profile.startAccountId = codexAccount.id;
|
|
40
59
|
const bridge = codexDesktopRendererBridge();
|
|
41
60
|
profile.rendererBridges = [bridge, ...profile.rendererBridges];
|
|
42
61
|
profile.providers = providersFromRendererBridges(profile.rendererBridges);
|
|
@@ -81,13 +100,18 @@ function codexDesktopRendererBridge() {
|
|
|
81
100
|
export function printLaunchStatus(profile, host, options = {}) {
|
|
82
101
|
const mode = options.mockMode ? "Mock transcript" : "Codex Desktop";
|
|
83
102
|
console.log("");
|
|
84
|
-
console.log(`ClankerBend: ${profile.name}`);
|
|
103
|
+
console.log(`ClankerBend profile: ${profile.launchProfileName || profile.name || "Default"}`);
|
|
85
104
|
console.log(`Status: ${mode} is running`);
|
|
86
|
-
console.log(`Panel: ${host.state.panel.url}`);
|
|
87
105
|
console.log(`Host: ${host.state.host.url}`);
|
|
88
|
-
if (
|
|
89
|
-
console.log("Codex Desktop is open. Use the Browser side panel for ClankerBend apps.");
|
|
90
|
-
}
|
|
106
|
+
if (host.token) console.log(`Token: ${host.token}`);
|
|
91
107
|
console.log("Press Ctrl+C to stop ClankerBend and the launched Codex Desktop process.");
|
|
92
108
|
console.log("");
|
|
93
109
|
}
|
|
110
|
+
|
|
111
|
+
function profileNameFromId(profileId) {
|
|
112
|
+
return String(profileId || "default")
|
|
113
|
+
.split(/[-_\s]+/)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
116
|
+
.join(" ") || "Default";
|
|
117
|
+
}
|
package/launch/runtime-paths.mjs
CHANGED
|
@@ -15,6 +15,9 @@ export function clankerbendRuntimePaths(options = {}) {
|
|
|
15
15
|
root,
|
|
16
16
|
runDir: join(root, "run"),
|
|
17
17
|
registryConfigPath: join(root, "registry.json"),
|
|
18
|
+
accountRegistryPath: join(root, "accounts.json"),
|
|
19
|
+
accountProfilesDir: join(root, "accounts"),
|
|
20
|
+
deletedAccountProfilesDir: join(root, "deleted-accounts"),
|
|
18
21
|
appInstallDir: join(root, "apps"),
|
|
19
22
|
codexProfileDir: join(root, "codex-profile")
|
|
20
23
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ClankerBendHost, createMockTranscriptAdapter } from "../host/src/index.js";
|
|
6
|
+
import { CodexAccountRegistry, PRIMARY_ACCOUNT_ID } from "./accounts.mjs";
|
|
7
|
+
|
|
8
|
+
const root = mkdtempSync(join(tmpdir(), "clankerbend-accounts-test-"));
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const primaryHome = join(root, "primary-codex-home");
|
|
12
|
+
const primaryElectronProfile = join(root, "primary-electron-profile");
|
|
13
|
+
mkdirSync(primaryHome, { recursive: true });
|
|
14
|
+
mkdirSync(primaryElectronProfile, { recursive: true });
|
|
15
|
+
writeFileSync(join(primaryHome, "config.toml"), "model = \"gpt-5.4\"\n");
|
|
16
|
+
writeFileSync(join(primaryHome, "auth.json"), JSON.stringify({ auth_mode: "chatgpt", tokens: {} }));
|
|
17
|
+
|
|
18
|
+
const registry = new CodexAccountRegistry({
|
|
19
|
+
root: join(root, "state"),
|
|
20
|
+
primaryCodexHome: primaryHome,
|
|
21
|
+
primaryElectronProfile
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const listed = registry.list();
|
|
25
|
+
assert.equal(listed.accounts.length, 1);
|
|
26
|
+
assert.equal(listed.accounts[0].id, PRIMARY_ACCOUNT_ID);
|
|
27
|
+
assert.equal(listed.accounts[0].codexHome, primaryHome);
|
|
28
|
+
|
|
29
|
+
const work = registry.createManaged({ id: "work", label: "Work" });
|
|
30
|
+
assert.equal(work.id, "work");
|
|
31
|
+
assert.equal(existsSync(work.codexHome), true);
|
|
32
|
+
assert.equal(existsSync(work.electronProfile), true);
|
|
33
|
+
assert.equal(readFileSync(join(work.codexHome, "config.toml"), "utf8"), "cli_auth_credentials_store = \"file\"\n");
|
|
34
|
+
|
|
35
|
+
const duplicateLabel = registry.createManaged({ label: "Work" });
|
|
36
|
+
assert.equal(duplicateLabel.id, "work-2");
|
|
37
|
+
const numericLabel = registry.createManaged({ label: "123" });
|
|
38
|
+
assert.equal(numericLabel.id, "account-123");
|
|
39
|
+
|
|
40
|
+
writeFileSync(join(work.codexHome, "auth.json"), JSON.stringify({ auth_mode: "chatgpt", tokens: { account: "work" } }));
|
|
41
|
+
const adoption = registry.adoptAsPrimary("work");
|
|
42
|
+
assert.equal(existsSync(primaryHome), true);
|
|
43
|
+
assert.equal(JSON.parse(readFileSync(join(primaryHome, "auth.json"), "utf8")).tokens.account, "work");
|
|
44
|
+
assert.equal(adoption.previousPrimary.kind, "managed");
|
|
45
|
+
assert.equal(existsSync(adoption.backup.codexHome), true);
|
|
46
|
+
assert.equal(registry.list().accounts.some((account) => account.id === "work"), false);
|
|
47
|
+
|
|
48
|
+
const rollback = registry.rollbackPrimary({ backupId: adoption.previousPrimary.id });
|
|
49
|
+
assert.equal(rollback.restoredAccountId, adoption.previousPrimary.id);
|
|
50
|
+
assert.equal(JSON.parse(readFileSync(join(primaryHome, "auth.json"), "utf8")).tokens.account, undefined);
|
|
51
|
+
assert.equal(registry.list().accounts.some((account) => account.id === rollback.replacedPrimary.id), true);
|
|
52
|
+
|
|
53
|
+
const temp = registry.createManaged({ id: "temp", label: "Temp" });
|
|
54
|
+
const deleted = registry.deleteManaged("temp");
|
|
55
|
+
assert.equal(registry.list().accounts.some((account) => account.id === "temp"), false);
|
|
56
|
+
assert.equal(existsSync(deleted.deletedRoot), true);
|
|
57
|
+
assert.equal(existsSync(deleted.codexHome), true);
|
|
58
|
+
|
|
59
|
+
const fresh = registry.createManaged({ id: "fresh", label: "Fresh" });
|
|
60
|
+
const host = new ClankerBendHost({
|
|
61
|
+
accountRegistry: registry,
|
|
62
|
+
transcriptAdapter: createMockTranscriptAdapter()
|
|
63
|
+
});
|
|
64
|
+
assert.equal(host.publicState().codexAccounts.accounts.find((account) => account.id === "fresh").auth.authJson, false);
|
|
65
|
+
writeFileSync(join(fresh.codexHome, "auth.json"), JSON.stringify({ auth_mode: "chatgpt", tokens: { account: "fresh" } }));
|
|
66
|
+
assert.equal(host.publicState().codexAccounts.accounts.find((account) => account.id === "fresh").auth.authJson, true);
|
|
67
|
+
|
|
68
|
+
console.log("clankerbend account registry tests passed");
|
|
69
|
+
} finally {
|
|
70
|
+
rmSync(root, { recursive: true, force: true });
|
|
71
|
+
}
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
-
import { createCodexDesktopCdpAdapter } from "./host/src/codex-desktop-cdp-adapter.js";
|
|
3
2
|
import { ClankerBendHost, createMockTranscriptAdapter } from "./host/src/index.js";
|
|
3
|
+
import { createCodexDesktopMuxAdapter } from "./launch/codex-mux-adapter.mjs";
|
|
4
4
|
import { navigateProfile, printLaunchStatus } from "./launch/profiles.mjs";
|
|
5
5
|
|
|
6
6
|
export async function launchClankerBendCodex(options = {}) {
|
|
@@ -10,7 +10,9 @@ export async function launchClankerBendCodex(options = {}) {
|
|
|
10
10
|
stateDir: options.stateDir,
|
|
11
11
|
runDir: options.runDir,
|
|
12
12
|
registryConfigPath: options.registryConfigPath,
|
|
13
|
-
registryProfileId: options.registryProfileId
|
|
13
|
+
registryProfileId: options.registryProfileId,
|
|
14
|
+
accountId: options.accountId,
|
|
15
|
+
primaryCodexHome: options.primaryCodexHome
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
const host = new ClankerBendHost({
|
|
@@ -19,13 +21,18 @@ export async function launchClankerBendCodex(options = {}) {
|
|
|
19
21
|
token: options.token || process.env.ONEWILL_CLANKERBEND_TOKEN || undefined,
|
|
20
22
|
localDevInsecure,
|
|
21
23
|
runDir: profile.runDir,
|
|
24
|
+
accountRegistry: profile.accountRegistry,
|
|
22
25
|
transcriptAdapter: mockMode
|
|
23
26
|
? createMockTranscriptAdapter({ defaultAppId: profile.defaultPanelAppId, providers: profile.providers })
|
|
24
|
-
:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
: createCodexDesktopMuxAdapter({
|
|
28
|
+
accountRegistry: profile.accountRegistry,
|
|
29
|
+
startAccountId: profile.startAccountId,
|
|
30
|
+
providers: profile.providers,
|
|
31
|
+
adapterOptions: {
|
|
32
|
+
runDir: profile.runDir,
|
|
33
|
+
rendererBridges: profile.rendererBridges,
|
|
34
|
+
providers: profile.providers
|
|
35
|
+
}
|
|
29
36
|
})
|
|
30
37
|
});
|
|
31
38
|
|