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/host/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # OneWhack Host
2
+
3
+ OneWhack is an independent OneWill project compatible with OpenAI Codex
4
+ Desktop. It is not affiliated with or endorsed by OpenAI.
5
+
6
+ This package contains reusable host primitives for OneWhack `0.1`:
7
+
8
+ - loopback HTTP server
9
+ - JSON response envelopes and error envelopes
10
+ - app registry and app-scoped manifests/state/actions
11
+ - action idempotency by `(appId, actionId)`
12
+ - SSE state/action/heartbeat events
13
+ - transcript anchor state
14
+ - monotonic selection handling
15
+ - mock transcript adapter for tests and local development
16
+
17
+ The host does not contain Vim Navigator-specific logic. Apps register a
18
+ manifest, static panel directory, state function, and action handler.
@@ -0,0 +1,315 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ export const ONEWHACK_APP_MANIFEST_VERSION = "0.1";
6
+
7
+ const VALID_DISTRIBUTION_KINDS = new Set(["local", "npm", "tarball", "binary"]);
8
+ const VALID_PLATFORM_OS = new Set(["darwin", "linux", "win32", "any"]);
9
+ const VALID_ENTRYPOINT_KINDS = new Set(["module", "binary", "static"]);
10
+ const VALID_CAPABILITIES = new Set([
11
+ "panel",
12
+ "annotations",
13
+ "commands",
14
+ "actions",
15
+ "appState",
16
+ "rendererBridge",
17
+ "selectionActions",
18
+ "overlays",
19
+ "composerContext",
20
+ "composerDraft"
21
+ ]);
22
+ const VALID_PERMISSIONS = new Set([
23
+ "transcriptRead",
24
+ "transcriptAnnotate",
25
+ "transcriptNavigate",
26
+ "overlayWrite",
27
+ "composerWrite",
28
+ "appServerRead",
29
+ "appServerApprove",
30
+ "appServerRollback"
31
+ ]);
32
+ const PROVIDER_NAMES = [
33
+ "transcriptSnapshot",
34
+ "transcriptOrder",
35
+ "transcriptNavigation",
36
+ "transcriptHighlight",
37
+ "transcriptTextSelection",
38
+ "composerDraft",
39
+ "composerContext"
40
+ ];
41
+
42
+ export function readAppManifest(manifestPath) {
43
+ const absolutePath = resolve(manifestPath);
44
+ return validateAppManifest(JSON.parse(readFileSync(absolutePath, "utf8")), {
45
+ manifestPath: absolutePath
46
+ });
47
+ }
48
+
49
+ export function validateAppManifest(manifest, options = {}) {
50
+ const errors = [];
51
+ if (!isPlainObject(manifest)) errors.push("manifest must be a JSON object");
52
+ if (errors.length) throwManifestError(errors, options);
53
+
54
+ requireString(manifest, "oneWhackVersion", errors);
55
+ if (manifest.oneWhackVersion !== ONEWHACK_APP_MANIFEST_VERSION) {
56
+ errors.push(`oneWhackVersion must be ${ONEWHACK_APP_MANIFEST_VERSION}`);
57
+ }
58
+ requireString(manifest, "appId", errors);
59
+ requireString(manifest, "version", errors);
60
+ requireString(manifest, "name", errors);
61
+
62
+ if (!isPlainObject(manifest.distribution)) {
63
+ errors.push("distribution must be an object");
64
+ } else {
65
+ if (!VALID_DISTRIBUTION_KINDS.has(manifest.distribution.kind)) {
66
+ errors.push(`distribution.kind must be one of ${[...VALID_DISTRIBUTION_KINDS].join(", ")}`);
67
+ }
68
+ if (manifest.distribution.source !== undefined && typeof manifest.distribution.source !== "string") {
69
+ errors.push("distribution.source must be a string when present");
70
+ }
71
+ requireString(manifest.distribution, "integrity", errors, "distribution.integrity");
72
+ if (manifest.distribution.update !== undefined && !isPlainObject(manifest.distribution.update)) {
73
+ errors.push("distribution.update must be an object when present");
74
+ }
75
+ }
76
+
77
+ if (!isPlainObject(manifest.entrypoint)) {
78
+ errors.push("entrypoint must be an object");
79
+ } else {
80
+ if (!VALID_ENTRYPOINT_KINDS.has(manifest.entrypoint.kind)) {
81
+ errors.push(`entrypoint.kind must be one of ${[...VALID_ENTRYPOINT_KINDS].join(", ")}`);
82
+ }
83
+ if (manifest.entrypoint.kind === "module") {
84
+ requireString(manifest.entrypoint, "module", errors, "entrypoint.module");
85
+ requireString(manifest.entrypoint, "factory", errors, "entrypoint.factory");
86
+ }
87
+ if (manifest.entrypoint.kind === "binary") {
88
+ requireString(manifest.entrypoint, "command", errors, "entrypoint.command");
89
+ }
90
+ if (manifest.entrypoint.publicDir !== undefined && typeof manifest.entrypoint.publicDir !== "string") {
91
+ errors.push("entrypoint.publicDir must be a string when present");
92
+ }
93
+ }
94
+
95
+ if (!isPlainObject(manifest.platform)) {
96
+ errors.push("platform must be an object");
97
+ } else {
98
+ const os = manifest.platform.os || ["any"];
99
+ if (!Array.isArray(os) || !os.length || os.some((value) => !VALID_PLATFORM_OS.has(value))) {
100
+ errors.push(`platform.os must contain ${[...VALID_PLATFORM_OS].join(", ")}`);
101
+ }
102
+ }
103
+
104
+ validateBooleanMap(manifest.capabilities, "capabilities", VALID_CAPABILITIES, errors);
105
+ validateBooleanMap(manifest.permissions, "permissions", VALID_PERMISSIONS, errors);
106
+
107
+ if (manifest.panel !== undefined && !isPlainObject(manifest.panel)) {
108
+ errors.push("panel must be an object when present");
109
+ }
110
+ if (manifest.rendererBridge !== undefined) validateRendererBridgeManifest(manifest.rendererBridge, errors);
111
+ if (manifest.lifecycle !== undefined) validateLifecycleManifest(manifest.lifecycle, errors);
112
+
113
+ if (errors.length) throwManifestError(errors, options);
114
+ return manifest;
115
+ }
116
+
117
+ export async function loadRegisteredApp(manifestPath) {
118
+ const absoluteManifestPath = resolve(manifestPath);
119
+ const manifest = readAppManifest(absoluteManifestPath);
120
+ const baseDir = dirname(absoluteManifestPath);
121
+ const app = manifest.entrypoint.kind === "module"
122
+ ? await loadModuleApp(manifest, baseDir)
123
+ : createPackagedApp(manifest, baseDir);
124
+ return {
125
+ manifest,
126
+ app: normalizeAppFromManifest(app, manifest),
127
+ rendererBridge: manifest.rendererBridge ? rendererBridgeFromManifest(manifest, absoluteManifestPath) : null
128
+ };
129
+ }
130
+
131
+ export async function loadProfileFromManifests(options) {
132
+ const loadedApps = [];
133
+ for (const manifestPath of uniqueStrings(options.manifestPaths || [])) {
134
+ loadedApps.push(await loadRegisteredApp(manifestPath));
135
+ }
136
+ const apps = loadedApps.map((loaded) => loaded.app);
137
+ const rendererBridges = loadedApps.map((loaded) => loaded.rendererBridge).filter(Boolean);
138
+ const providers = providersFromRendererBridges(rendererBridges);
139
+ const defaultPanelAppId = options.defaultPanelAppId || apps.find((app) => app.contributes?.panel)?.appId || apps[0]?.appId || null;
140
+ return {
141
+ profileId: options.profileId,
142
+ name: options.name,
143
+ description: options.description,
144
+ hostId: options.hostId,
145
+ hostName: options.hostName,
146
+ runDir: options.runDir,
147
+ defaultPanelAppId,
148
+ apps,
149
+ manifests: loadedApps.map((loaded) => loaded.manifest),
150
+ rendererBridges,
151
+ providers
152
+ };
153
+ }
154
+
155
+ export function enabledManifestPathsFromConfig(config, profileId = "default") {
156
+ const enabledAppIds = config.profiles?.[profileId]?.enabledAppIds || [];
157
+ return enabledAppIds
158
+ .map((appId) => config.installedApps?.[appId]?.manifestPath)
159
+ .filter(Boolean);
160
+ }
161
+
162
+ export function mergeManifestPaths(...groups) {
163
+ return uniqueStrings(groups.flat().filter(Boolean).map((value) => resolve(value)));
164
+ }
165
+
166
+ export function providersFromRendererBridges(rendererBridges) {
167
+ const providers = {};
168
+ const primary = rendererBridges.find((bridge) => bridge.primary) || rendererBridges[0] || null;
169
+ for (const name of PROVIDER_NAMES) {
170
+ providers[name] = rendererBridges.find((bridge) => bridge.provides?.includes(name))?.appId || primary?.appId || null;
171
+ }
172
+ return providers;
173
+ }
174
+
175
+ export function rendererBridgeFromManifest(manifest, manifestPath) {
176
+ const baseDir = dirname(resolve(manifestPath));
177
+ const bridge = manifest.rendererBridge;
178
+ return {
179
+ appId: manifest.appId,
180
+ injectedScriptPath: resolve(baseDir, bridge.script),
181
+ openPanelMethod: bridge.methods?.openPanel || "openPanel",
182
+ scrollMethod: bridge.methods?.scroll || "scrollToAnchor",
183
+ highlightMethod: bridge.methods?.highlight || "highlightAnchor",
184
+ primary: bridge.primary === true,
185
+ provides: bridge.provides || []
186
+ };
187
+ }
188
+
189
+ export function normalizeAppFromManifest(app, manifest) {
190
+ if (!app || typeof app !== "object") throw new Error(`${manifest.appId} factory did not return an app object`);
191
+ if (app.appId !== manifest.appId) throw new Error(`${manifest.appId} factory returned mismatched appId: ${app.appId}`);
192
+ return {
193
+ ...app,
194
+ name: app.name || manifest.name,
195
+ version: app.version || manifest.version,
196
+ contributes: app.contributes || manifest.capabilities || {},
197
+ permissions: app.permissions || manifest.permissions || {},
198
+ panel: app.panel || manifest.panel || undefined,
199
+ manifest
200
+ };
201
+ }
202
+
203
+ export function defaultRegistryConfig() {
204
+ return {
205
+ oneWhackVersion: ONEWHACK_APP_MANIFEST_VERSION,
206
+ installedApps: {},
207
+ profiles: {
208
+ default: {
209
+ enabledAppIds: []
210
+ }
211
+ }
212
+ };
213
+ }
214
+
215
+ export function loadRegistryConfig(configPath) {
216
+ if (!existsSync(configPath)) return defaultRegistryConfig();
217
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
218
+ if (!isPlainObject(config.installedApps)) config.installedApps = {};
219
+ if (!isPlainObject(config.profiles)) config.profiles = defaultRegistryConfig().profiles;
220
+ return config;
221
+ }
222
+
223
+ async function loadModuleApp(manifest, baseDir) {
224
+ const modulePath = resolve(baseDir, manifest.entrypoint.module);
225
+ const imported = await import(pathToFileURL(modulePath));
226
+ const factory = imported[manifest.entrypoint.factory];
227
+ if (typeof factory !== "function") throw new Error(`${manifest.appId} factory not found: ${manifest.entrypoint.factory}`);
228
+ return factory({
229
+ manifest,
230
+ publicDir: manifest.entrypoint.publicDir ? resolve(baseDir, manifest.entrypoint.publicDir) : undefined
231
+ });
232
+ }
233
+
234
+ function createPackagedApp(manifest, baseDir) {
235
+ return {
236
+ appId: manifest.appId,
237
+ name: manifest.name,
238
+ version: manifest.version,
239
+ publicDir: manifest.entrypoint.publicDir ? resolve(baseDir, manifest.entrypoint.publicDir) : undefined,
240
+ contributes: manifest.capabilities || {},
241
+ permissions: manifest.permissions || {},
242
+ panel: manifest.panel,
243
+ getState(context) {
244
+ return {
245
+ appId: manifest.appId,
246
+ status: manifest.entrypoint.kind === "binary" ? "degraded" : "ready",
247
+ source: manifest.entrypoint.kind,
248
+ connected: manifest.entrypoint.kind !== "binary",
249
+ entries: manifest.entrypoint.kind === "binary"
250
+ ? [{
251
+ entryId: `${manifest.appId}:binary-not-started`,
252
+ appId: manifest.appId,
253
+ title: manifest.name,
254
+ summary: "Binary app lifecycle is installed but not started by this in-process host.",
255
+ category: "lifecycle"
256
+ }]
257
+ : [],
258
+ updatedAt: context.state.generatedAt
259
+ };
260
+ }
261
+ };
262
+ }
263
+
264
+ function validateRendererBridgeManifest(bridge, errors) {
265
+ if (!isPlainObject(bridge)) {
266
+ errors.push("rendererBridge must be an object when present");
267
+ return;
268
+ }
269
+ requireString(bridge, "script", errors, "rendererBridge.script");
270
+ if (bridge.methods !== undefined && !isPlainObject(bridge.methods)) errors.push("rendererBridge.methods must be an object when present");
271
+ if (bridge.provides !== undefined) {
272
+ if (!Array.isArray(bridge.provides) || bridge.provides.some((name) => !PROVIDER_NAMES.includes(name))) {
273
+ errors.push(`rendererBridge.provides must contain ${PROVIDER_NAMES.join(", ")}`);
274
+ }
275
+ }
276
+ if (bridge.primary !== undefined && typeof bridge.primary !== "boolean") errors.push("rendererBridge.primary must be boolean when present");
277
+ }
278
+
279
+ function uniqueStrings(values) {
280
+ return [...new Set(values.map((value) => String(value)))];
281
+ }
282
+
283
+ function validateLifecycleManifest(lifecycle, errors) {
284
+ if (!isPlainObject(lifecycle)) {
285
+ errors.push("lifecycle must be an object when present");
286
+ return;
287
+ }
288
+ for (const hook of ["install", "start", "stop", "update", "remove"]) {
289
+ if (lifecycle[hook] !== undefined && !isPlainObject(lifecycle[hook])) errors.push(`lifecycle.${hook} must be an object when present`);
290
+ }
291
+ }
292
+
293
+ function validateBooleanMap(value, label, validKeys, errors) {
294
+ if (!isPlainObject(value)) {
295
+ errors.push(`${label} must be an object`);
296
+ return;
297
+ }
298
+ for (const [key, entry] of Object.entries(value)) {
299
+ if (!validKeys.has(key)) errors.push(`${label}.${key} is not supported`);
300
+ if (typeof entry !== "boolean") errors.push(`${label}.${key} must be boolean`);
301
+ }
302
+ }
303
+
304
+ function requireString(object, key, errors, label = key) {
305
+ if (typeof object[key] !== "string" || object[key].trim().length === 0) errors.push(`${label} must be a non-empty string`);
306
+ }
307
+
308
+ function throwManifestError(errors, options = {}) {
309
+ const prefix = options.manifestPath ? `${options.manifestPath}: ` : "";
310
+ throw new Error(`${prefix}invalid OneWhack app manifest:\n${errors.map((error) => `- ${error}`).join("\n")}`);
311
+ }
312
+
313
+ function isPlainObject(value) {
314
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
315
+ }