golem-kit 0.2.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - An app pins the model its terminal agents run on: `chat: { provider: 'tmux', agent, model }` for normal-mode chat and `agents.builderModel` for the builder. The pin rides the launch args (`-m` for Codex, `--model` for Claude Code) and is replayed on resume.
6
+
7
+ ## 0.2.1
8
+
9
+ - `./golem` runs on a fresh `pnpm install`: the installed entry resolves `tsx` from golem-kit's own tree instead of the app's `node_modules/.bin`.
10
+ - `init` leaves a typecheckable app: a `tsconfig.json`, `src/globals.d.ts`, `dev`/`build`/`lint`/`typecheck` scripts, and the `typescript`, `@types/node`, `@types/react` devDependencies they need.
11
+ - `./golem build` sees the app's own `src/*.d.ts`, so an app can `import './app.css'` without `@ts-ignore`.
12
+ - The new-app scaffold uses only classes golem-ui's packaged stylesheet ships; an app styles the rest with its own CSS.
13
+
3
14
  ## 0.2.0
4
15
 
5
16
  - `golem-kit/server` export and an `exports` map, so an app imports the backend by name instead of by path.
package/docs/agents.md CHANGED
@@ -18,6 +18,7 @@ export default {
18
18
  title: 'Field Notes',
19
19
  agents: {
20
20
  builder: 'claude', // or 'codex': the agent build mode starts with
21
+ builderModel: 'claude-opus-5-5', // pins that CLI's model
21
22
  ordinary: {
22
23
  backend: 'anthropic',
23
24
  operations: ['records.list', 'records.get', 'records.update', 'notes.archive'],
@@ -29,6 +30,7 @@ export default {
29
30
  ```
30
31
 
31
32
  - `builder` is the default choice in build mode. A person's own pick in the browser still wins and is remembered.
33
+ - `builderModel` pins the builder CLI's model, in that CLI's own spelling (`--model` for Claude Code, `-m` for Codex). A terminal chat pins its own the same way: `chat: { provider: 'tmux', agent: 'codex', model: 'gpt-6-luna' }`. Both survive a resume. Left out, each CLI picks its default.
32
34
  - `ordinary` turns on the **Start a chat** button. Leave it out and there is no ordinary chat.
33
35
  - `ordinary.model` defaults to `claude-opus-5`.
34
36
  - `golem.config.ts` is bundled into the browser. Keep the API key in `.env.local` or the server environment, never in this file.
@@ -13,6 +13,8 @@ Business rules stay in the app; this package supplies storage, the operation bou
13
13
  | `src/server/index.ts` | server | `golem-kit/server`, `src/shared/`, `src/server/**` |
14
14
  | `src/server/persistence/` | server | the only place for backend-specific code (`records.native`) |
15
15
 
16
+ App styling is the app's own: golem-ui ships a packaged stylesheet holding only the classes its components use, so anything beyond those is CSS written in `src/` and imported from UI code.
17
+
16
18
  UI code reaches data only through `golem-kit/client`. Secrets come from `process.env` (loaded from `.env.local`) inside `src/server/`; `golem.config.ts` is bundled into the browser, so it holds no secrets.
17
19
 
18
20
  ## Configuration
@@ -92,7 +94,9 @@ type Model = { extract<S extends z.ZodType>(request: { schema: S; text: string;
92
94
  const meeting = await model.extract({ schema: z.object({ date: z.string(), attendees: z.array(z.string()) }), text, instructions: 'Leave a field empty rather than guessing.' })
93
95
  ```
94
96
 
95
- The app names the shape it wants and never which model answered. Today that is the local Claude
97
+ The app names the shape it wants and never which model answered. Which runtime does is the app's
98
+ `model: { runtime, name }` — `{ runtime: 'codex', name: 'gpt-6-luna' }` is the cheap everyday pick
99
+ for extraction and scheduled jobs. Absent, it is the local Claude
96
100
  Code CLI — the runtime build mode already depends on, and the one that asks for no API key; the prompt
97
101
  goes in on stdin and the call runs in a temporary directory, so neither `ps` nor the app's folder
98
102
  is part of it. When no model can answer — the runtime is missing, times out, or gives nothing the
package/docs/builder.md CHANGED
@@ -6,7 +6,7 @@ Read `docs/domain.md`, the app's DNA, first, and update it in the same change as
6
6
 
7
7
  For implementation and pull-request work, start with the user or business problem and the resulting behavior. Keep a PR description to one short paragraph; add terse validation and dependencies only when useful. Run checks appropriate to the change.
8
8
 
9
- Supported app surface: edit `src/app.tsx` (it may also `export const screens = [{ id, label, icon? }]`; each one gets an item in the bottom menu row and the chosen id arrives as the `screen` prop); configure the shell title, host, port, storage, optional accounts, and optional agents in `golem.config.ts` (read `node_modules/golem-kit/docs/agents.md` before turning on ordinary chat); use `./golem help`, `./golem build`, `./golem lint`, and `./golem dev`. The installed `golem-ui` package is the component contract: its `README.md` names the current components and adapters, while its API and adapter docs are linked there. Use its `config` plus `adapters` shape rather than inventing a data layer inside a component. Ask before changing an important application contract or proposing framework/UI-kit work.
9
+ Supported app surface: edit `src/app.tsx` (it may also `export const screens = [{ id, label, icon? }]`; each one gets an item in the bottom menu row and the chosen id arrives as the `screen` prop); configure the shell title, host, port, storage, optional accounts, and optional agents in `golem.config.ts` (read `node_modules/golem-kit/docs/agents.md` before turning on ordinary chat); use `./golem help`, `./golem build`, `./golem lint`, and `./golem dev`. The installed `golem-ui` package is the component contract: its `README.md` names the current components and adapters, while its API and adapter docs are linked there. Use its `config` plus `adapters` shape rather than inventing a data layer inside a component. Only the utility classes golem-ui's own components use exist in its packaged stylesheet, so any styling beyond those is CSS this app writes and imports from `src/`. Ask before changing an important application contract or proposing framework/UI-kit work.
10
10
 
11
11
  Before storing records or files, adding server behavior an agent or the UI calls, or adding sign-in, roles or groups, read `node_modules/golem-kit/docs/app-backend.md`: it names where UI, shared, and server code live and the operation, authorization, and storage contracts.
12
12
 
package/docs/local-cli.md CHANGED
@@ -55,9 +55,12 @@ and rebuilds the shared `dist/` directory; requires port 3000).
55
55
 
56
56
  ## Generated projects
57
57
 
58
- `golem-kit init` creates `package.json`, `golem.config.ts`, `eslint.config.mjs`, `src/app.tsx`,
59
- `docs/domain.md` (the app's DNA template), small `AGENTS.md` and `CLAUDE.md` pointers to the
60
- installed framework guide, and an executable `./golem`. Normal initialization writes the
58
+ `golem-kit init` creates `package.json`, `golem.config.ts`, `tsconfig.json`, `eslint.config.mjs`,
59
+ `src/app.tsx`, `src/globals.d.ts`, `docs/domain.md` (the app's DNA template), small `AGENTS.md` and
60
+ `CLAUDE.md` pointers to the installed framework guide, and an executable `./golem`. The generated
61
+ `package.json` carries the scripts `dev`, `build`, `lint` (each one the matching `./golem` command)
62
+ and `typecheck` (`tsc --noEmit` over `tsconfig.json`), plus the `typescript`, `@types/node` and
63
+ `@types/react` devDependencies those scripts need at the versions golem-kit itself uses. Normal initialization writes the
61
64
  pinned npm dependency `golem-kit@<framework version>` and installs it with pnpm, along with
62
65
  the exact `golem-ui` version golem-kit uses so app code can import its components directly.
63
66
  For local packed-tarball acceptance only, set `GOLEM_KIT_TARBALL=/path/to/golem-kit.tgz`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "golem-kit",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Starter kit and local CLI for Golem applications.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,12 +39,17 @@ export async function buildBrowser(): Promise<void> {
39
39
  noEmit: true, jsx: 'react-jsx', module: 'ESNext', moduleResolution: 'Bundler',
40
40
  skipLibCheck: true, allowImportingTsExtensions: true,
41
41
  types: ['node', 'react', 'react-dom'], typeRoots: [typeRoots],
42
- paths: sourcePaths(),
42
+ // The same two aliases vite.config.ts resolves, because `include` now sweeps in any shell
43
+ // file a checkout used as its own app keeps beside the app's.
44
+ paths: {
45
+ ...sourcePaths(),
46
+ '@golem/app': [resolve(process.cwd(), 'src/app.tsx')],
47
+ '@golem/config': [resolve(process.cwd(), 'golem.config.ts')],
48
+ },
43
49
  },
44
- files: [
45
- resolve(process.cwd(), 'src/app.tsx'), resolve(process.cwd(), 'golem.config.ts'),
46
- ...[resolve(process.cwd(), 'src/server/index.ts')].filter(existsSync),
47
- ],
50
+ // `include`, not a `files` list: the app's own ambient declarations (`src/*.d.ts`, so that
51
+ // `import './app.css'` resolves) only enter the program when the whole of `src/` does.
52
+ include: [resolve(process.cwd(), 'src/**/*'), resolve(process.cwd(), 'golem.config.ts')],
48
53
  }));
49
54
  execFileSync(tsc, ['-p', appTsconfig], { cwd: process.cwd(), stdio: 'inherit' });
50
55
  await build({ configFile: resolve(frameworkRoot, 'vite.config.ts') });
package/src/cli.ts CHANGED
@@ -116,7 +116,7 @@ See node_modules/golem-kit/docs/architecture.md to add eslint.config.mjs.`);
116
116
 
117
117
  function initProject(): void {
118
118
  const root = resolve(process.cwd());
119
- const files = ['golem.config.ts', 'eslint.config.mjs', 'src/app.tsx', 'docs/domain.md', 'AGENTS.md', 'CLAUDE.md', 'golem', 'brain/index.md', 'brain/log.md'];
119
+ const files = ['golem.config.ts', 'tsconfig.json', 'eslint.config.mjs', 'src/app.tsx', 'src/globals.d.ts', 'docs/domain.md', 'AGENTS.md', 'CLAUDE.md', 'golem', 'brain/index.md', 'brain/log.md'];
120
120
  const existing = files.filter((file) => existsSync(resolve(root, file)));
121
121
  if (existing.length) throw new Error(`refusing to overwrite existing files: ${existing.join(', ')}`);
122
122
  const packagePath = resolve(root, 'package.json');
@@ -134,10 +134,27 @@ function initProject(): void {
134
134
  writeFileSync(packagePath, JSON.stringify({
135
135
  name: 'golem-app', private: true, type: 'module', packageManager: 'pnpm@10.28.2',
136
136
  engines: { node: '>=22.18.0', pnpm: '10.28.2' },
137
+ scripts: { dev: './golem dev', build: './golem build', lint: './golem lint', typecheck: 'tsc --noEmit' },
137
138
  ...(process.env.GOLEM_KIT_TARBALL ? {} : { dependencies: { 'golem-kit': framework.version } }),
139
+ // The app's own tsconfig needs a compiler and the React/Node types in the app's tree;
140
+ // golem-kit has them for its own program, and pnpm does not share them with the app.
141
+ devDependencies: Object.fromEntries(['typescript', '@types/node', '@types/react']
142
+ .map((name) => [name, framework.dependencies[name]])),
138
143
  }, null, 2) + '\n');
139
144
  }
140
145
  writeFileSync(resolve(root, 'golem.config.ts'), "export default { title: 'Golem', brain: true }\n");
146
+ writeFileSync(resolve(root, 'tsconfig.json'), JSON.stringify({
147
+ compilerOptions: {
148
+ target: 'ES2023', module: 'ESNext', moduleResolution: 'Bundler',
149
+ strict: true, noEmit: true, allowImportingTsExtensions: true,
150
+ jsx: 'react-jsx', skipLibCheck: true, types: ['node'],
151
+ },
152
+ include: ['src/**/*', 'golem.config.ts'],
153
+ }, null, 2) + '\n');
154
+ writeFileSync(resolve(root, 'src/globals.d.ts'), `// golem-ui ships its own compiled stylesheet; any class beyond the ones its components use is
155
+ // CSS this app writes and imports itself.
156
+ declare module '*.css'
157
+ `);
141
158
  writeFileSync(resolve(root, 'brain/index.md'), `---
142
159
  okf_version: "0.2"
143
160
  ---
@@ -148,7 +165,7 @@ This folder is an Open Knowledge Format bundle: one concept per markdown file wi
148
165
  writeFileSync(resolve(root, 'brain/log.md'), '# Log\n');
149
166
  writeFileSync(resolve(root, 'src/app.tsx'), `export default function App() {
150
167
  return (
151
- <section className="flex h-full min-h-64 items-center justify-center bg-neutral-50 p-6 text-center">
168
+ <section className="flex h-full items-center justify-center bg-neutral-50 p-6 text-center">
152
169
  <div>
153
170
  <h1 className="text-lg font-semibold">Welcome to Golem</h1>
154
171
  <p className="mt-2 text-sm text-neutral-500">Edit src/app.tsx to build your app.</p>
package/src/config.ts CHANGED
@@ -12,14 +12,14 @@ export type ModelConfig = { runtime: 'claude' | 'codex'; name?: string }
12
12
  * terminal agent in the `chat` window of the app's tmux session, briefed from `docs/chat.md`.
13
13
  * `roles` restricts chat to those account roles; absent, anyone signed in may chat.
14
14
  */
15
- export type ChatConfig = ({ provider: 'anthropic' } | { provider: 'tmux'; agent?: 'codex' | 'claude' }) & { roles?: string[] }
15
+ export type ChatConfig = ({ provider: 'anthropic' } | { provider: 'tmux'; agent?: 'codex' | 'claude'; model?: string }) & { roles?: string[] }
16
16
  /** `brain: true` serves the app's `brain/` folder read-only and mounts the Brain reader beside the app. */
17
17
 
18
18
  /**
19
19
  * `builder` is the agent build mode starts with. `ordinary` turns on everyday chat: an API agent
20
20
  * whose only tools are the listed app operations, run as the person chatting.
21
21
  */
22
- export type AgentsConfig = { builder?: 'codex' | 'claude'; ordinary?: OrdinaryAgentConfig }
22
+ export type AgentsConfig = { builder?: 'codex' | 'claude'; builderModel?: string; ordinary?: OrdinaryAgentConfig }
23
23
  export type OrdinaryAgentConfig = { backend: 'anthropic'; model: string; operations: string[]; collections?: string[]; roots?: string[]; instructions?: string }
24
24
 
25
25
  /** golem-ui's Auth role shape: `manages` roles run accounts and may build; `builder` may build. */
@@ -70,10 +70,11 @@ export async function loadAppConfig(root = process.cwd()): Promise<AppConfig> {
70
70
  config.agents = { ...config.agents, ordinary: ordinaryAgent({ backend: 'anthropic', ...rest }) }
71
71
  config.chat = { provider, ...chatRoles }
72
72
  } else if (provider === 'tmux') {
73
- const { agent, ...unknown } = rest
73
+ const { agent, model, ...unknown } = rest
74
74
  if (Object.keys(unknown).length) throw new Error(`golem.config.ts chat has unknown fields: ${Object.keys(unknown).join(', ')}`)
75
75
  if (agent !== undefined && agent !== 'codex' && agent !== 'claude') throw new Error("golem.config.ts chat.agent must be 'codex' or 'claude'")
76
- config.chat = { provider, ...(agent ? { agent } : {}), ...chatRoles }
76
+ if (model !== undefined && (typeof model !== 'string' || !model)) throw new Error('golem.config.ts chat.model must be a nonempty string')
77
+ config.chat = { provider, ...(agent ? { agent } : {}), ...(model ? { model: model as string } : {}), ...chatRoles }
77
78
  } else throw new Error("golem.config.ts chat.provider must be 'anthropic' or 'tmux'")
78
79
  } else if (config.agents?.ordinary) config.chat = { provider: 'anthropic' }
79
80
  return config
@@ -117,10 +118,11 @@ function chatRolesOf(value: unknown, accounts: AccountsConfig | undefined): stri
117
118
 
118
119
  function agents(value: unknown): AgentsConfig {
119
120
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('golem.config.ts agents must be an object')
120
- const { builder, ordinary, ...unknown } = value as Record<string, unknown>
121
+ const { builder, builderModel, ordinary, ...unknown } = value as Record<string, unknown>
121
122
  if (Object.keys(unknown).length) throw new Error(`golem.config.ts agents has unknown fields: ${Object.keys(unknown).join(', ')}`)
122
123
  if (builder !== undefined && builder !== 'codex' && builder !== 'claude') throw new Error("golem.config.ts agents.builder must be 'codex' or 'claude'")
123
- return { ...(builder ? { builder } : {}), ...(ordinary === undefined ? {} : { ordinary: ordinaryAgent(ordinary) }) }
124
+ if (builderModel !== undefined && (typeof builderModel !== 'string' || !builderModel)) throw new Error('golem.config.ts agents.builderModel must be a nonempty string')
125
+ return { ...(builder ? { builder } : {}), ...(builderModel ? { builderModel: builderModel as string } : {}), ...(ordinary === undefined ? {} : { ordinary: ordinaryAgent(ordinary) }) }
124
126
  }
125
127
 
126
128
  // Operations whose input or output is raw bytes, which a chat tool cannot carry.
package/src/dev-server.ts CHANGED
@@ -45,7 +45,8 @@ export async function startDevServer(
45
45
  // A new session needs a runnable CLI; a restored one keeps its ref and resumes on its next message.
46
46
  createBackend ??= async (backend, ref, buildMode = true) => {
47
47
  if (!ref && !(await discoverAgents()).some((found) => found.agent === backend && found.runnable)) throw new Error(`${backend} is not runnable here`);
48
- return new TmuxBackend(appRoot, backend, ref, { stateDir: join(stateDirectory, 'harness'), api: serverUrl(host, port), window: buildMode ? 'builder' : 'chat', ...(buildMode ? {} : { instructions: chatInstructions(appRoot), permissions: 'readonly' }) });
48
+ const model = buildMode ? app.config.agents?.builderModel : app.config.chat?.provider === 'tmux' ? app.config.chat.model : undefined;
49
+ return new TmuxBackend(appRoot, backend, ref, { stateDir: join(stateDirectory, 'harness'), api: serverUrl(host, port), window: buildMode ? 'builder' : 'chat', ...(model ? { model } : {}), ...(buildMode ? {} : { instructions: chatInstructions(appRoot), permissions: 'readonly' }) });
49
50
  };
50
51
  const builder = await builderFlag(stateDirectory);
51
52
  const state = new ConversationState(stateDirectory);
package/src/entry.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process'
2
2
  import { existsSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
3
4
  import { dirname, resolve } from 'node:path'
4
5
  import { fileURLToPath } from 'node:url'
5
6
 
@@ -8,11 +9,14 @@ const source = process.env.GOLEM_SOURCE && resolve(process.env.GOLEM_SOURCE)
8
9
  const cli = source ? resolve(source, 'src/cli.ts') : resolve(frameworkRoot, 'src/cli.ts')
9
10
  if (!existsSync(cli)) throw new Error(`GOLEM_SOURCE must point to a Golem checkout containing src/cli.ts: ${source}`)
10
11
 
12
+ // Node does not strip types from files under node_modules, so the installed CLI needs tsx. Resolve
13
+ // it from this file — golem-kit's own dependency tree — because pnpm does not link a transitive
14
+ // dependency's bin into the app's node_modules/.bin, so the app's PATH may not have tsx at all.
11
15
  const installed = !source && frameworkRoot.includes('/node_modules/')
12
- const command = installed ? resolve(process.cwd(), 'node_modules/.bin/golem-kit') : process.execPath
13
- const args = installed ? process.argv.slice(2) : [cli, ...process.argv.slice(2)]
14
- const env = installed ? { ...process.env, PATH: `${resolve(process.cwd(), 'node_modules/.bin')}:${process.env.PATH ?? ''}` } : process.env
15
- const child = spawn(command, args, { cwd: process.cwd(), env, stdio: 'inherit' })
16
+ const args = installed
17
+ ? [createRequire(import.meta.url).resolve('tsx/cli'), cli, ...process.argv.slice(2)]
18
+ : [cli, ...process.argv.slice(2)]
19
+ const child = spawn(process.execPath, args, { cwd: process.cwd(), stdio: 'inherit' })
16
20
  for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => child.kill(signal))
17
21
  const result = await new Promise((resolve, reject) => child.once('error', reject).once('exit', (code, signal) => resolve({ code, signal })))
18
22
  if (result.signal) process.kill(process.pid, result.signal)
@@ -53,9 +53,10 @@ export const brainInstructions = 'This app has a brain: `brain/` is an Open Know
53
53
  /**
54
54
  * `window` names this agent's window in the app's session (`builder`, `chat`); `instructions` its launch prompt;
55
55
  * `permissions` its launch profile: `bypass` (default) may do anything, `readonly` can read the app and run
56
- * `./golem say`, nothing else, and refuses rather than prompts.
56
+ * `./golem say`, nothing else, and refuses rather than prompts. `model` pins the agent's model, in that
57
+ * CLI's own spelling (`gpt-6-luna` for codex, `claude-opus-5-5` for claude).
57
58
  */
58
- export type TmuxOptions = { harness?: Harness; stateDir?: string; api?: string; window?: string; instructions?: string; permissions?: 'bypass' | 'readonly' }
59
+ export type TmuxOptions = { harness?: Harness; stateDir?: string; api?: string; window?: string; instructions?: string; permissions?: 'bypass' | 'readonly'; model?: string }
59
60
 
60
61
  /** The one tmux session of an app's build mode: `tmux attach -t golem-<app dir>` is always the place to look. */
61
62
  export const tmuxSessionName = (cwd: string): string => `golem-${basename(cwd).replace(/[^A-Za-z0-9_-]/g, '-')}`
@@ -96,7 +97,10 @@ export class TmuxBackend implements SessionBackend {
96
97
  // codex 0.155: the update prompt at launch would take the typed brief as its answer, the
97
98
  // paste-burst fold swallows the first Enter of a long line, and the rate-limit "keep current
98
99
  // model" nudge after the first turn eats the first message. All off; replayed on resume.
99
- extraArgs: this.agent === 'codex' ? ['-c', 'check_for_update_on_startup=false', '-c', 'disable_paste_burst=true', '-c', 'notice.hide_rate_limit_model_nudge=true'] : [],
100
+ extraArgs: [
101
+ ...(this.agent === 'codex' ? ['-c', 'check_for_update_on_startup=false', '-c', 'disable_paste_burst=true', '-c', 'notice.hide_rate_limit_model_nudge=true'] : []),
102
+ ...(this.opts.model ? [this.agent === 'codex' ? '-m' : '--model', this.opts.model] : []),
103
+ ],
100
104
  }
101
105
  try {
102
106
  // A ref saved under an older naming (`golem-<uuid>`) comes back in the app's fixed session: only