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 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` binds only to `127.0.0.1:3000`.
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 only. Port 3000 must be free.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "golem-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Starter kit and local CLI for Golem applications.",
5
5
  "type": "module",
6
6
  "files": [
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 at http://127.0.0.1:3000/.
16
- Stop with Ctrl+C. Uses the local Codex runtime from the browser.
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
- No network exposure is enabled; the dev server binds to loopback.`);
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 server = await startDevServer();
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('Golem shell: http://127.0.0.1:3000/ (Ctrl+C to stop)');
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, '127.0.0.1', () => resolve(server));
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 === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
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 });