golem-kit 0.1.0 → 0.1.1
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 +3 -1
- package/docs/local-cli.md +12 -3
- package/package.json +1 -1
- package/src/cli.ts +7 -5
- package/src/config.ts +35 -0
- package/src/dev-server.ts +12 -8
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ Start the application:
|
|
|
24
24
|
|
|
25
25
|
Open the browser address printed in the terminal, enter build mode, and start a conversation. Build chat requires the Codex CLI installed and authenticated on the same machine. After a successful build-mode change, the browser shell rebuilds and refreshes; conversations are saved in `.golem/` and restored when the server restarts.
|
|
26
26
|
|
|
27
|
-
`./golem build` writes the browser shell to `dist/`. `./golem dev`
|
|
27
|
+
`./golem build` writes the browser shell to `dist/`. `./golem dev` defaults to
|
|
28
|
+
`127.0.0.1:3000`; set an optional `host` and `port` in `golem.config.ts`, then restart
|
|
29
|
+
the dev server. The terminal prints the usable address.
|
|
28
30
|
|
|
29
31
|
## Local source development
|
|
30
32
|
|
package/docs/local-cli.md
CHANGED
|
@@ -23,16 +23,25 @@ boundary so a hot-source workflow can select the CLI without changing HTTP start
|
|
|
23
23
|
| Command | Behavior | Exit status |
|
|
24
24
|
| --- | --- | --- |
|
|
25
25
|
| `./golem help` (or `./golem`) | List all commands | 0 |
|
|
26
|
-
| `./golem dev` | Refresh the browser build, then serve it at `http://127.0.0.1:3000/` until SIGINT/SIGTERM | 0 on clean shutdown; 1 on startup failure |
|
|
26
|
+
| `./golem dev` | Refresh the browser build, then serve it at the `golem.config.ts` address (default `http://127.0.0.1:3000/`) until SIGINT/SIGTERM | 0 on clean shutdown; 1 on startup failure |
|
|
27
27
|
| `./golem build` | Build the browser shell into `dist/` | 0 |
|
|
28
28
|
| `./golem doctor` | Report local shell and backend readiness | 0 |
|
|
29
29
|
|
|
30
30
|
Unknown commands and extra arguments exit 2. Built assets are served directly; extensionless browser
|
|
31
31
|
routes fall back to `index.html`, while missing assets return 404. Malformed URLs return 400. The
|
|
32
|
-
server binds to loopback
|
|
32
|
+
default server binds to loopback on port 3000. To use another local port or a
|
|
33
|
+
specific tailnet address, export optional root settings from `golem.config.ts`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export default { title: 'Golem', host: '100.64.0.10', port: 3000 }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`host` must be a nonempty string and `port` an integer from 1 through 65535.
|
|
40
|
+
Configuration, build, and bind failures are printed by `./golem dev`. Restart
|
|
41
|
+
the server after changing these settings.
|
|
33
42
|
`doctor` succeeding means its report ran, not that the full product is ready.
|
|
34
43
|
|
|
35
|
-
`src/dev-server.ts` exports `startDevServer(port = 3000)`, resolving to a listening
|
|
44
|
+
`src/dev-server.ts` exports `startDevServer(port = 3000, backend, stateDirectory, host = '127.0.0.1')`, resolving to a listening
|
|
36
45
|
Node HTTP server. It refreshes the browser build before listening; the CLI owns signal handling and output. `src/browser/app.tsx` is the
|
|
37
46
|
composition boundary: anonymous identity and browser navigation are explicit host adapters, while
|
|
38
47
|
the chat adapter connects explicit browser build-mode sessions to the local Codex runtime.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { startDevServer } from './dev-server.ts';
|
|
3
3
|
import { buildBrowser } from './browser-build.ts';
|
|
4
|
+
import { loadAppConfig, serverUrl } from './config.ts';
|
|
4
5
|
import { execFileSync } from 'node:child_process';
|
|
5
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
7
|
import { dirname, resolve } from 'node:path';
|
|
@@ -12,8 +13,8 @@ Usage: ./golem <command>
|
|
|
12
13
|
|
|
13
14
|
help Show every command (also the default).
|
|
14
15
|
init Create a minimal app in the current directory.
|
|
15
|
-
dev Serve the browser shell
|
|
16
|
-
|
|
16
|
+
dev Serve the browser shell using golem.config.ts (127.0.0.1:3000 by default).
|
|
17
|
+
Restart after changing settings. Uses the local Codex runtime from the browser.
|
|
17
18
|
build Build the browser shell into dist/.
|
|
18
19
|
doctor Report local shell and backend readiness.
|
|
19
20
|
|
|
@@ -45,7 +46,7 @@ if (args.length || !['help', 'init', 'dev', 'build', 'doctor'].includes(command)
|
|
|
45
46
|
Ready: local CLI, HTTP shell, golem-ui browser build and Codex session seam.
|
|
46
47
|
Claude integration: not yet connected.
|
|
47
48
|
Not implemented: Claude integration.
|
|
48
|
-
|
|
49
|
+
The dev server defaults to 127.0.0.1:3000 and uses optional host/port from golem.config.ts.`);
|
|
49
50
|
break;
|
|
50
51
|
case 'build':
|
|
51
52
|
try {
|
|
@@ -57,7 +58,8 @@ No network exposure is enabled; the dev server binds to loopback.`);
|
|
|
57
58
|
break;
|
|
58
59
|
case 'dev':
|
|
59
60
|
try {
|
|
60
|
-
const
|
|
61
|
+
const config = await loadAppConfig();
|
|
62
|
+
const server = await startDevServer(config.port, undefined, undefined, config.host);
|
|
61
63
|
const stop = () => {
|
|
62
64
|
server.close((error) => {
|
|
63
65
|
process.removeListener('SIGINT', stop);
|
|
@@ -73,7 +75,7 @@ No network exposure is enabled; the dev server binds to loopback.`);
|
|
|
73
75
|
};
|
|
74
76
|
process.once('SIGINT', stop);
|
|
75
77
|
process.once('SIGTERM', stop);
|
|
76
|
-
console.log(
|
|
78
|
+
console.log(`Golem shell: ${serverUrl(config.host, config.port)} (Ctrl+C to stop)`);
|
|
77
79
|
} catch (error) {
|
|
78
80
|
console.error(`Cannot start Golem dev server: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
81
|
process.exitCode = 1;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
export type AppConfig = { host: string; port: number }
|
|
5
|
+
|
|
6
|
+
export async function loadAppConfig(): Promise<AppConfig> {
|
|
7
|
+
const path = resolve(process.cwd(), 'golem.config.ts')
|
|
8
|
+
let value: unknown
|
|
9
|
+
try {
|
|
10
|
+
value = (await import(pathToFileURL(path).href)).default
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error(`Cannot load golem.config.ts: ${message(error)}`)
|
|
13
|
+
}
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
15
|
+
throw new Error('golem.config.ts must default-export an object')
|
|
16
|
+
}
|
|
17
|
+
const configured = value as { host?: unknown; port?: unknown }
|
|
18
|
+
const host: unknown = configured.host === undefined ? '127.0.0.1' : configured.host
|
|
19
|
+
const port: unknown = configured.port === undefined ? 3000 : configured.port
|
|
20
|
+
if (typeof host !== 'string' || !host.trim()) {
|
|
21
|
+
throw new Error('golem.config.ts host must be a nonempty string')
|
|
22
|
+
}
|
|
23
|
+
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
24
|
+
throw new Error('golem.config.ts port must be an integer from 1 to 65535')
|
|
25
|
+
}
|
|
26
|
+
return { host, port }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function message(error: unknown): string {
|
|
30
|
+
return error instanceof Error ? error.message : String(error)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function serverUrl(host: string, port: number): string {
|
|
34
|
+
return `http://${host.includes(':') ? `[${host}]` : host}:${port}/`
|
|
35
|
+
}
|
package/src/dev-server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { discoverAgents, runtimeState } from './runtime/discovery.ts';
|
|
|
7
7
|
import { CodexBackend, type SandboxMode } from './runtime/codex.ts';
|
|
8
8
|
import { SessionManager, type Session, type SessionBackend } from './runtime/session.ts';
|
|
9
9
|
import { ConversationState } from './runtime/state.ts';
|
|
10
|
+
import { serverUrl } from './config.ts';
|
|
10
11
|
|
|
11
12
|
const appRoot = resolve(process.cwd());
|
|
12
13
|
const root = pathToFileURL(`${process.cwd()}/dist/`);
|
|
@@ -22,13 +23,14 @@ export async function startDevServer(
|
|
|
22
23
|
port = 3000,
|
|
23
24
|
createBackend: (mode: SandboxMode, threadId?: string) => SessionBackend = (mode, threadId) => new CodexBackend(appRoot, mode, 'codex', [], threadId),
|
|
24
25
|
stateDirectory = join(appRoot, '.golem'),
|
|
26
|
+
host = '127.0.0.1',
|
|
25
27
|
): Promise<Server> {
|
|
26
28
|
await buildBrowser();
|
|
27
29
|
const state = new ConversationState(stateDirectory);
|
|
28
30
|
const sessions = new SessionManager((snapshots) => state.save(snapshots));
|
|
29
31
|
sessions.restore(await state.load(), (snapshot) => createBackend(snapshot.buildMode ? 'danger-full-access' : 'read-only', snapshot.threadId));
|
|
30
32
|
const server = createServer((request, response) => {
|
|
31
|
-
void handleRequest(request, response, sessions, port, createBackend).catch((error) => {
|
|
33
|
+
void handleRequest(request, response, sessions, port, host, createBackend).catch((error) => {
|
|
32
34
|
if (!response.headersSent) json(response, 400, { error: error instanceof Error ? error.message : 'Malformed request' });
|
|
33
35
|
else response.destroy();
|
|
34
36
|
});
|
|
@@ -36,7 +38,7 @@ export async function startDevServer(
|
|
|
36
38
|
server.once('close', () => { void sessions.disposeAll().then(() => sessions.flushAll()) });
|
|
37
39
|
return new Promise((resolve, reject) => {
|
|
38
40
|
server.once('error', reject);
|
|
39
|
-
server.listen(port,
|
|
41
|
+
server.listen(port, host, () => resolve(server));
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -45,12 +47,13 @@ async function handleRequest(
|
|
|
45
47
|
response: import('node:http').ServerResponse,
|
|
46
48
|
sessions: SessionManager,
|
|
47
49
|
port: number,
|
|
50
|
+
host: string,
|
|
48
51
|
createBackend: (mode: SandboxMode, threadId?: string) => SessionBackend,
|
|
49
52
|
): Promise<void> {
|
|
50
53
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
51
54
|
decodeURIComponent(url.pathname);
|
|
52
55
|
if (url.pathname.startsWith('/api/')) {
|
|
53
|
-
await handleApi(request, response, url, sessions, port, createBackend);
|
|
56
|
+
await handleApi(request, response, url, sessions, port, host, createBackend);
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
let pathname: string;
|
|
@@ -112,9 +115,9 @@ function triggerRebuild(session: Session): void {
|
|
|
112
115
|
);
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
function mutationAllowed(request: import('node:http').IncomingMessage, port: number): boolean {
|
|
118
|
+
function mutationAllowed(request: import('node:http').IncomingMessage, port: number, host: string): boolean {
|
|
116
119
|
const origin = request.headers.origin;
|
|
117
|
-
return !origin || origin ===
|
|
120
|
+
return !origin || origin === serverUrl(host, port).slice(0, -1) || (host === '127.0.0.1' && origin === `http://localhost:${port}`);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
async function handleApi(
|
|
@@ -123,6 +126,7 @@ async function handleApi(
|
|
|
123
126
|
url: URL,
|
|
124
127
|
sessions: SessionManager,
|
|
125
128
|
port: number,
|
|
129
|
+
host: string,
|
|
126
130
|
createBackend: (mode: SandboxMode) => SessionBackend,
|
|
127
131
|
): Promise<void> {
|
|
128
132
|
if (request.method === 'GET' && url.pathname === '/api/runtime') {
|
|
@@ -131,7 +135,7 @@ async function handleApi(
|
|
|
131
135
|
return;
|
|
132
136
|
}
|
|
133
137
|
if (request.method === 'POST' && url.pathname === '/api/sessions') {
|
|
134
|
-
if (!mutationAllowed(request, port)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
138
|
+
if (!mutationAllowed(request, port, host)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
135
139
|
try {
|
|
136
140
|
const input = await body(request) as { backend?: string; intent?: string };
|
|
137
141
|
if (input.backend !== 'codex') return json(response, 400, { error: 'Only the connected Codex backend can start a session' });
|
|
@@ -165,7 +169,7 @@ async function handleApi(
|
|
|
165
169
|
return;
|
|
166
170
|
}
|
|
167
171
|
if (request.method === 'POST' && !match[2]) {
|
|
168
|
-
if (!mutationAllowed(request, port)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
172
|
+
if (!mutationAllowed(request, port, host)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
169
173
|
try {
|
|
170
174
|
const input = await body(request) as { text?: unknown };
|
|
171
175
|
if (typeof input.text !== 'string' || !input.text.trim()) return json(response, 400, { error: 'text must be a non-empty string' });
|
|
@@ -180,7 +184,7 @@ async function handleApi(
|
|
|
180
184
|
return;
|
|
181
185
|
}
|
|
182
186
|
if (request.method === 'POST' && match[2] === 'interrupt') {
|
|
183
|
-
if (!mutationAllowed(request, port)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
187
|
+
if (!mutationAllowed(request, port, host)) return json(response, 403, { error: 'Cross-origin mutations are not allowed' });
|
|
184
188
|
await session.interrupt();
|
|
185
189
|
await session.flush();
|
|
186
190
|
json(response, 200, { status: session.status });
|