@dhzh/foundry 0.0.3-beta.2 → 0.0.3-beta.3

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
@@ -1,139 +1,136 @@
1
- # @dhzh/foundry
1
+ # Foundry
2
2
 
3
- An AI-native local developer runtime for tools, skills, agents, and workflows.
3
+ Foundry is an Electron desktop application for managing local AI agent Runtime configurations. The current release focuses on custom Provider management for Codex and Claude Code: define connection and model settings once, preview the resulting configuration changes, and apply them to the corresponding local Runtime.
4
4
 
5
- Foundry is distributed as a CLI package. It starts a local Hono server and
6
- serves the bundled React Web UI from `dist/web/`.
5
+ > Foundry is in an early stage. Provider and Runtime management are functional; the Skills area is currently a placeholder, and broader tools, agents, and workflows remain future work.
7
6
 
8
- The Web UI is built with Astryx and provides the local Foundry administration
9
- shell, including the Settings surface.
7
+ Tagged builds are published on the [GitHub Releases](https://github.com/tinywaves/foundry/releases) page. The current automated release workflow produces an unsigned, unnotarized macOS Universal DMG, so macOS may require explicit approval before opening it.
10
8
 
11
- ## Requirements
9
+ ## Current Features
12
10
 
13
- - Node.js `^24.18.0`
14
- - pnpm `^11.9.0`
11
+ - Manage separate custom Providers for Codex and Claude Code.
12
+ - Store Provider names, Base URLs, API keys, model mappings, remarks, websites, and optional avatars locally.
13
+ - Test Provider connectivity before or after saving.
14
+ - View Provider health and Runtime status from the dashboard.
15
+ - Select a Provider or restore Official Default settings for each Runtime.
16
+ - Preview every managed configuration field before writing to disk, with secrets redacted by default.
17
+ - Preserve settings outside Foundry's managed fields and keep a backup of the previous configuration.
18
+ - Automatically reapply an in-use Provider after a Runtime-effective edit.
19
+ - Show persistent restart guidance after every successful configuration write.
20
+ - On macOS, optionally restart an already-running ChatGPT desktop app so its hosted Codex experience reloads the configuration.
15
21
 
16
- ## Setup
22
+ ## Supported Runtimes
17
23
 
18
- ```sh
19
- pnpm install
20
- ```
24
+ | Runtime | Configuration file | Managed configuration | Reload behavior |
25
+ | --- | --- | --- | --- |
26
+ | Codex | `~/.codex/config.toml` | Model selection and a Foundry-managed `model_providers` entry | Foundry can gracefully restart an already-running ChatGPT desktop app on macOS. Existing Codex CLI sessions must be restarted manually. |
27
+ | Claude Code | `~/.claude/settings.json` | Anthropic endpoint, token, model-role mappings, fallback model, and subagent model under `env` | Existing Claude Code CLI sessions must be restarted manually. |
21
28
 
22
- ## Development
29
+ Restoring Official Default removes the Runtime selection overrides managed by Foundry. It does not delete saved Provider records or unrelated Runtime settings.
23
30
 
24
- Start the Web UI development server with Rsbuild:
31
+ ## Typical Workflow
25
32
 
26
- ```sh
27
- pnpm run dev:web
28
- ```
33
+ 1. Open **Agent Runtime > Providers** and add a Provider for Codex or Claude Code.
34
+ 2. Enter the endpoint, API key, and Runtime-specific model settings.
35
+ 3. Run **Test Connection** to verify that the endpoint responds successfully.
36
+ 4. Open **Agent Runtime > Runtimes**, select the desired Provider, and choose **Apply**.
37
+ 5. Review the exact field-level changes in the preview dialog and confirm the write.
38
+ 6. Follow the result dialog to restart the affected desktop application or CLI session.
29
39
 
30
- Rsbuild opens the browser at [http://localhost:3000](http://localhost:3000).
40
+ When an in-use Provider is edited, Foundry saves the Provider first and reapplies it only when a Runtime-effective field changed. If the reapply fails, the saved Provider remains available and the dialog offers the existing Apply retry flow.
31
41
 
32
- To run the CLI against the local Web UI, build the static assets first and then
33
- start the CLI in watch mode:
42
+ ## Connection Tests
34
43
 
35
- ```sh
36
- pnpm run build:web
37
- pnpm run dev:cli
38
- ```
44
+ Provider tests run in the Electron main process with a 15-second timeout and do not follow redirects.
39
45
 
40
- The CLI serves the Web UI at [http://127.0.0.1:7777](http://127.0.0.1:7777).
46
+ - Codex sends `GET <base-url>/models` and uses `Authorization: Bearer <api-key>` when a key is configured.
47
+ - Claude Code sends `GET <base-url>/v1/models`, or `GET <base-url>/models` when the Base URL already ends in `/v1`. It sends `anthropic-version: 2023-06-01` and uses `x-api-key` when configured.
48
+ - Any `2xx` response passes. Network, TLS, timeout, redirect, and non-`2xx` outcomes are recorded as sanitized connection failures.
41
49
 
42
- ## Commands
50
+ ## Configuration Safety
43
51
 
44
- Run these commands from the repository root:
52
+ Foundry treats configuration writes as a previewed and recoverable operation:
45
53
 
46
- ```sh
47
- pnpm run build # Build the CLI and Web UI
48
- pnpm run build:cli # Build only dist/cli/
49
- pnpm run build:web # Build only dist/web/
50
- pnpm run dev:cli # Run the CLI from source in watch mode
51
- pnpm run dev:web # Start the Rsbuild Web UI development server
52
- pnpm run lint # Run ESLint
53
- ```
54
+ - Existing TOML or JSON is parsed before any change is offered.
55
+ - Only the documented Runtime fields are modified; unrelated settings are preserved.
56
+ - The proposed output is generated, parsed again, and validated before replacing the active file.
57
+ - Writes use same-directory temporary files with restrictive `0600` permissions.
58
+ - The latest previous file content is stored beside the Runtime configuration as `<filename>.foundry-backup`.
59
+ - If the configuration is written but Foundry cannot record the new application state, it restores the previous file content.
60
+ - Concurrent writes to the same Runtime are rejected.
61
+
62
+ Provider records, including API keys and avatars, are stored in a local SQLite database named `foundry.sqlite` under Electron's platform-specific `userData` directory. Applied API keys are also written to the target Runtime configuration because the Runtime requires them. Treat both the Foundry user-data directory and Runtime configuration files as sensitive local data.
63
+
64
+ ## ChatGPT Restart Behavior
65
+
66
+ Automatic restart is intentionally narrow and available only on macOS:
67
+
68
+ - ChatGPT is identified by the fixed bundle identifier `com.openai.codex`.
69
+ - Foundry offers restart only after confirming that ChatGPT is already running.
70
+ - Restart requests a normal application termination and never force-quits ChatGPT.
71
+ - Foundry waits up to 15 seconds for exit, reopens the same bundle, and waits up to another 15 seconds to confirm startup.
72
+ - If detection, quit, or reopen fails, the applied configuration remains intact and Foundry shows manual guidance without an automatic retry action.
73
+ - Foundry never launches ChatGPT when it was not already running and never starts or terminates Codex CLI or Claude Code CLI processes.
54
74
 
55
- ## Run A Production Build
75
+ Restarting ChatGPT affects the entire desktop application and may interrupt work in its Chat, Work, and Codex views.
56
76
 
57
- Build the package and launch the executable wrapper:
77
+ ## Development
78
+
79
+ ### Prerequisites
80
+
81
+ - Node.js `24.18.0`
82
+ - pnpm `11.9.0`
58
83
 
59
- ```sh
60
- pnpm run build
61
- node bin/index.js
84
+ ### Install and Run
85
+
86
+ ```bash
87
+ pnpm install
88
+ pnpm dev
62
89
  ```
63
90
 
64
- To use the published package without cloning the repository:
91
+ ### Verify Changes
65
92
 
66
- ```sh
67
- pnpm dlx @dhzh/foundry
93
+ ```bash
94
+ pnpm test
95
+ pnpm typecheck
96
+ pnpm lint
97
+ pnpm build
68
98
  ```
69
99
 
70
- The `foundry` executable listens on `127.0.0.1:7777` and prints a clickable
71
- local URL when it starts.
100
+ ### Package the Application
72
101
 
73
- ## Settings
102
+ ```bash
103
+ pnpm build:mac
104
+ pnpm build:win
105
+ pnpm build:linux
106
+ ```
74
107
 
75
- The Settings capability is available through the Web UI and the module-level
76
- CLI command:
108
+ `pnpm build:unpack` creates an unpacked application directory for local packaging checks. Pushing a `v*` tag runs the release workflow, builds a macOS Universal DMG, and attaches it to a GitHub Release. Automated release artifacts are not currently signed or notarized.
77
109
 
78
- Current registered settings are `ui.theme` and `ui.pointer`.
110
+ ## Architecture
79
111
 
80
- ```sh
81
- foundry settings list
82
- foundry settings get ui.theme
83
- foundry settings set ui.theme dark
84
- foundry settings reset ui.theme
112
+ ```text
113
+ src/main/ Electron lifecycle, SQLite storage, configuration writes,
114
+ connection tests, and native ChatGPT control
115
+ src/preload/ Narrow contextBridge APIs for Provider and Runtime operations
116
+ src/renderer/ React UI, page workflows, and renderer-side response validation
117
+ src/shared/ Typed Provider and Runtime contracts shared across processes
118
+ resources/ Runtime packaging assets
119
+ build/ electron-builder resources and macOS entitlements
85
120
  ```
86
121
 
87
- Use `--raw` when only command values or boolean mutation results are needed.
88
- The installable [`foundry-settings`](skills/foundry-settings/SKILL.md) Skill is
89
- the automation wrapper for this CLI surface.
90
-
91
- ## Web UI
92
-
93
- The private [`web`](packages/web/package.json) workspace uses:
94
-
95
- - [Rsbuild](https://rsbuild.rs) 2
96
- - React 19
97
- - React Compiler through [`@rsbuild/plugin-react`](https://rsbuild.rs/plugins/list/plugin-react)
98
- - Astryx components and the neutral theme
99
- - TypeScript with bundler module resolution
100
-
101
- The Web entrypoint is
102
- [`packages/web/src/index.tsx`](packages/web/src/index.tsx), which imports the
103
- global Astryx reset and theme styles from
104
- [`packages/web/src/index.css`](packages/web/src/index.css). The root component
105
- lives in [`packages/web/src/app.tsx`](packages/web/src/app.tsx).
106
-
107
- Rsbuild is configured in
108
- [`packages/web/rsbuild.config.ts`](packages/web/rsbuild.config.ts). Production
109
- assets are written directly to the repository-level `dist/web/` directory so
110
- the CLI can serve them next to its bundled output.
111
-
112
- ## Project Layout
113
-
114
- | Path | Role |
115
- |------|------|
116
- | `specs/plans/` | Numbered implementation plans and their naming convention |
117
- | `src/cli/` | CLI entrypoint and Hono static server |
118
- | `src/modules/settings/` | Settings registry, repository, service, routes, and CLI command |
119
- | `skills/foundry-settings/` | Installable Skill wrapping the Settings CLI command |
120
- | `packages/web/` | Rsbuild + React Web UI workspace |
121
- | `packages/web/src/index.tsx` | Browser entrypoint |
122
- | `packages/web/src/app.tsx` | Root React component |
123
- | `packages/web/src/index.css` | Astryx reset and theme imports |
124
- | `packages/web/rsbuild.config.ts` | React Compiler and Web build configuration |
125
- | `bin/index.js` | Published `foundry` executable wrapper |
126
- | `dist/cli/` | Bundled CLI output generated by tsdown |
127
- | `dist/web/` | Web UI production output served by the CLI |
128
-
129
- ## Implementation Plans
130
-
131
- See [`specs/plans/README.md`](specs/plans/README.md) for the canonical plan
132
- location, naming, numbering, and content conventions.
133
-
134
- ## Learn More
135
-
136
- - [Rsbuild documentation](https://rsbuild.rs)
137
- - [Rspack documentation](https://rspack.rs)
138
- - Run `pnpm exec astryx docs` for Astryx component and token guidance
139
- - [Repository guidelines for agents](AGENTS.md)
122
+ The renderer does not receive arbitrary filesystem, process, shell, or IPC access. Native operations remain in the main process and are exposed through purpose-specific preload methods with constrained inputs and validated responses.
123
+
124
+ ## Technology
125
+
126
+ - Electron and `electron-vite`
127
+ - React 19 and TypeScript
128
+ - Astryx Design System and StyleX
129
+ - TanStack Query
130
+ - SQLite through `better-sqlite3`
131
+ - Vitest and ESLint
132
+ - `electron-builder`
133
+
134
+ ## License
135
+
136
+ Foundry is licensed under the [Apache License 2.0](./LICENSE).
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export default function Test() {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dhzh/foundry",
3
3
  "type": "module",
4
- "version": "0.0.3-beta.2",
4
+ "version": "0.0.3-beta.3",
5
5
  "devEngines": {
6
6
  "packageManager": {
7
7
  "name": "pnpm",
@@ -26,11 +26,7 @@
26
26
  "cli",
27
27
  "ai"
28
28
  ],
29
- "bin": {
30
- "foundry": "./bin/index.js"
31
- },
32
29
  "files": [
33
- "bin",
34
30
  "dist"
35
31
  ],
36
32
  "repository": {
package/bin/index.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/cli/index.mjs';
@@ -1,25 +0,0 @@
1
- import e from"cac";import t,{exit as n}from"node:process";import r from"terminal-link";import{z as i}from"zod";import{existsSync as a,mkdirSync as o}from"node:fs";import{DatabaseSync as s}from"node:sqlite";import{homedir as c}from"node:os";import l from"node:path";import u from"cli-table3";import{consola as d}from"consola";import{serve as f}from"@hono/node-server";import{serveStatic as p}from"@hono/node-server/serve-static";import{zValidator as m}from"@hono/zod-validator";import{Hono as h}from"hono";function g(){return l.join(c(),`.foundry`)}function _(){return l.join(g(),`foundry.sqlite`)}function v(){let e=g();return a(e)||o(e,{recursive:!0}),e}function y(){let e=_();return a(e)||new s(e).close(),e}function b(){return{storageRootPath:v(),databasePath:y()}}function x(e){let t=e.prepare(`PRAGMA integrity_check`).all().flatMap(e=>Object.values(e));if(t.length!==1||t[0]!==`ok`)throw Error(`SQLite database integrity check failed`)}function S(e,t){if(!e.isOpen)throw Error(`Cannot start a transaction on a closed database`);if(e.isTransaction)throw Error(`Nested SQLite transactions are not supported`);e.exec(`BEGIN`);try{let n=t();return e.exec(`COMMIT`),n}catch(t){throw e.exec(`ROLLBACK`),t}}function C(){let e=_(),t=new s(e,{enableForeignKeyConstraints:!0,timeout:5e3});t.exec(`
2
- PRAGMA journal_mode = DELETE;
3
- PRAGMA synchronous = FULL;
4
- `);let n=!1;return{database:t,databasePath:e,assertIntegrity:()=>x(t),transaction:e=>S(t,e),close:()=>{n||=(t.close(),!0)}}}const w=[{group:`ui`,name:`theme`,defaultValue:`system`,schema:i.enum([`system`,`light`,`dark`]),secret:!1},{group:`ui`,name:`pointer`,defaultValue:!0,schema:i.boolean(),secret:!1}];function T(e,t){return w.find(n=>n.group===e&&n.name===t)}function E(e,t){return`${e}.${t}`}const D=`settings`;function O(e){e.database.exec(`
5
- CREATE TABLE IF NOT EXISTS ${D} (
6
- "group" TEXT NOT NULL,
7
- name TEXT NOT NULL,
8
- payload TEXT NOT NULL,
9
- created_at INTEGER NOT NULL,
10
- updated_at INTEGER NOT NULL,
11
- PRIMARY KEY ("group", name)
12
- )
13
- `);let t=e.database.prepare(`
14
- INSERT OR IGNORE INTO ${D} ("group", name, payload, created_at, updated_at)
15
- VALUES (?, ?, ?, ?, ?)
16
- `),n=Date.now();e.transaction(()=>{for(let e of w)t.run(e.group,e.name,JSON.stringify({value:e.defaultValue}),n,n)})}function k(e){let t=(e=[])=>`
17
- SELECT * FROM ${D}
18
- ${e.length>0?`WHERE ${e.map(e=>`${e} = ?`).join(` AND `)}`:``}
19
- `,n=e.database.prepare(t()),r=e.database.prepare(t([`"group"`,`name`])),i=e.database.prepare(`
20
- INSERT INTO ${D} ("group", name, payload, created_at, updated_at)
21
- VALUES (?, ?, ?, ?, ?)
22
- ON CONFLICT("group", name) DO UPDATE SET
23
- payload = excluded.payload,
24
- updated_at = excluded.updated_at
25
- `);return{get({group:e,name:t}){return r.get(e,t)},getAll(){return n.all()},upsert(e,t,n){let r=Date.now();return i.run(e,t,n,r,r).changes===1}}}function A(e,t){let n=T(e,t);if(!n)throw Error(`Unknown setting: ${e}.${t}`);return n}function j(e,t){let n;try{n=JSON.parse(e)}catch{return{structurallyValid:!1}}let r=i.object({value:t}).safeParse(n);return r.success?{value:r.data.value,structurallyValid:!0}:{structurallyValid:!1}}function M(e){O(e);let t=k(e);return{get(e,n){let r=t.get({group:e,name:n}),i=A(e,n),a=E(e,n);if(r){let t=j(r.payload,i.schema);if(t.structurallyValid)return{key:a,group:e,name:n,value:t.value}}return{key:a,group:e,name:n,value:i.defaultValue}},list(){return t.getAll().map(({group:e,name:t,payload:n})=>{let r=A(e,t),i=j(n,r.schema);return{key:E(e,t),group:e,name:t,value:i.structurallyValid?i.value:r.defaultValue}})},setMany(n){e.transaction(()=>{for(let{group:e,name:r,value:i}of n)t.upsert(e,r,JSON.stringify({value:i}))})},resetMany(n){let r=n.map(({group:e,name:t})=>A(e,t));e.transaction(()=>{for(let e of r)t.upsert(e.group,e.name,JSON.stringify({value:e.defaultValue}))})}}}function N(){let e=b(),t=C();try{return t.assertIntegrity(),{storage:t,storagePaths:e,settingsService:M(t),close:t.close}}catch(e){throw t.close(),e}}const P={log(...e){d.log(...e)},raw(...e){d.log.raw(...e)},table(e,t){let n=new u({head:[...e]});for(let e of t)n.push([...e]);d.log(n.toString())},info(...e){d.info(...e)},error(...e){d.error(...e)}},F=i.enum([`get`,`list`,`set`,`reset`]),I=[`Key`,`Value`];function L(e){return typeof e==`string`?e:JSON.stringify(e)}function R(e,t,n){if(e.length!==t)throw Error(`Usage: foundry settings ${n}`)}function z(e){let t=e.indexOf(`.`);if(t<=0||t>=e.length-1)throw Error(`Invalid setting key: ${e}`);return{group:e.slice(0,t),name:e.slice(t+1)}}function B(e){e.command(`settings <action> [...values]`,`Get, list, set, or reset Foundry settings`).option(`--raw`,`Print raw output`).action((e,t,n)=>{let r=F.safeParse(e);if(!r.success)throw Error(`Unknown settings action: ${e}, expected one of: ${F.options.join(`, `)}`);let{raw:i=!1}=n,a=N();try{switch(r.data){case`get`:{R(t,1,`get <key>`);let{group:e,name:n}=z(t[0]),r=a.settingsService.get(e,n);i?P.raw(L(r.value)):P.table(I,[[r.key,L(r.value)]]);return}case`list`:{R(t,0,`list`);let e=a.settingsService.list();if(i)for(let t of e)P.raw(L(t.value));else P.table(I,e.map(e=>[e.key,L(e.value)]));return}case`reset`:{R(t,1,`reset <key>`);let{group:e,name:n}=z(t[0]);try{a.settingsService.resetMany([{group:e,name:n}]),i?P.raw(`true`):P.log(!0)}catch{i?P.raw(`false`):P.log(!1)}return}case`set`:{R(t,2,`set <key> <value>`);let{group:e,name:n}=z(t[0]);try{a.settingsService.setMany([{group:e,name:n,value:t[1]}]),i?P.raw(`true`):P.log(!0)}catch{i?P.raw(`false`):P.log(!1)}}}}finally{a.close()}})}const V=e=>new h().get(`/api/settings`,t=>t.json(e.list())).post(`/api/settings`,m(`json`,i.array(i.object({group:i.string().min(1),name:i.string().min(1),value:i.json()})),(e,t)=>{if(!e.success)return t.json({error:`Invalid settings input`,issues:e.error.issues},400)}),t=>{let n=t.req.valid(`json`);try{return e.setMany(n),t.json(!0)}catch{return t.json({error:`Failed to update settings`},500)}}).post(`/api/settings/reset`,m(`json`,i.object({keys:i.array(i.object({group:i.string().min(1),name:i.string().min(1)}))}),(e,t)=>{if(!e.success)return t.json({error:`Invalid settings input`,issues:e.error.issues},400)}),t=>{let n=t.req.valid(`json`);try{return e.resetMany(n.keys),t.json(!0)}catch{return t.json({error:`Failed to reset settings`},500)}});function H(){let e=l.resolve(import.meta.dirname,`../web`);a(l.join(e,`index.html`))||(P.error(`Web UI assets missing. Please reinstall Foundry: npm install -g @dhzh/foundry`),n(1));let t=N(),r=V(t.settingsService);r.use(`/*`,p({root:e}));try{let e=f({fetch:r.fetch,hostname:`127.0.0.1`,port:7777});return{server:e,url:`http://127.0.0.1:7777`,close:()=>{e.close(),t.close()}}}catch(e){throw t.close(),e}}const U=e(`foundry`);B(U),U.command(`[...args]`,`Open a local Foundry workspace`).action(()=>{let e=H(),t=r(e.url,e.url,{fallback:!1});P.info(`Foundry workspace is running at ${t}`)}),U.help();try{U.parse()}catch(e){P.error(e instanceof Error?e.message:String(e)),t.exitCode=1}export{};
@@ -1 +0,0 @@
1
- <!DOCTYPE html><html><head><title>Foundry</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/lib-react.8689a73639.js"></script><script defer src="/static/js/lib-router.7aa1b84d62.js"></script><script defer src="/static/js/304.ae01979a4b.js"></script><script defer src="/static/js/index.93ad4f2a33.js"></script><link href="/static/css/304.399a9465c7.css" rel="stylesheet"><link href="/static/css/index.400108c990.css" rel="stylesheet"></head><body><div id="root"></div></body></html>