@aion0/forge 0.10.6 → 0.10.12

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/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,8 @@
1
- # Forge v0.10.6
1
+ # Forge v0.10.12
2
2
 
3
3
  Released: 2026-05-30
4
4
 
5
- ## Changes since v0.10.5
5
+ ## Changes since v0.10.11
6
6
 
7
- ### Other
8
- - feat(settings): folder picker for Add Project / Add Document Directory
9
7
 
10
-
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.5...v0.10.6
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.11...v0.10.12
@@ -29,6 +29,7 @@ import AdmZip from 'adm-zip';
29
29
  import { homedir } from 'node:os';
30
30
  import { getDb } from '@/src/core/db/database';
31
31
  import { getDbPath } from '@/src/config';
32
+ import { getClaudeDir } from '@/lib/dirs';
32
33
 
33
34
  /**
34
35
  * Refuse to clobber an existing skill (registry-synced or previously
@@ -52,7 +53,7 @@ function db() { return getDb(getDbPath()); }
52
53
  // ─── Helpers ──────────────────────────────────────────────
53
54
 
54
55
  function getClaudeHome(): string {
55
- return process.env.CLAUDE_HOME || join(homedir(), '.claude');
56
+ return getClaudeDir();
56
57
  }
57
58
 
58
59
  function isValidName(s: string): boolean {
package/cli/mw.mjs CHANGED
@@ -745,7 +745,7 @@ __export(dirs_exports, {
745
745
  });
746
746
  import { homedir as homedir2 } from "node:os";
747
747
  import { join as join3 } from "node:path";
748
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, copyFileSync, readFileSync as readFileSync2 } from "node:fs";
748
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, copyFileSync } from "node:fs";
749
749
  function getConfigDir() {
750
750
  return join3(homedir2(), ".forge");
751
751
  }
@@ -797,17 +797,7 @@ function migrateDataDir() {
797
797
  console.log("[forge] Migration complete. Old files kept as backup.");
798
798
  }
799
799
  function getClaudeDir() {
800
- if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
801
- try {
802
- const settingsFile = join3(getDataDir(), "settings.yaml");
803
- if (existsSync3(settingsFile)) {
804
- const raw = readFileSync2(settingsFile, "utf-8");
805
- const m = raw.match(/^\s*claudeHome:\s*["']?(.+?)["']?\s*$/m);
806
- if (m?.[1]) return m[1].replace(/^~/, homedir2());
807
- }
808
- } catch {
809
- }
810
- return join3(homedir2(), ".claude");
800
+ return process.env.CLAUDE_HOME || join3(homedir2(), ".claude");
811
801
  }
812
802
  var MIGRATE_FILES, MIGRATE_DIRS, migrated;
813
803
  var init_dirs = __esm({
@@ -835,10 +825,10 @@ var BASE2 = process.env.MW_URL || `http://localhost:${_cliPort || "3000"}`;
835
825
  var [, , cmd, ...args] = process.argv;
836
826
  async function checkForUpdate() {
837
827
  try {
838
- const { readFileSync: readFileSync3 } = await import("node:fs");
828
+ const { readFileSync: readFileSync2 } = await import("node:fs");
839
829
  const { join: join4, dirname } = await import("node:path");
840
830
  const { fileURLToPath } = await import("node:url");
841
- const pkg = JSON.parse(readFileSync3(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
831
+ const pkg = JSON.parse(readFileSync2(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
842
832
  const current = pkg.version;
843
833
  const controller = new AbortController();
844
834
  const timeout = setTimeout(() => controller.abort(), 3e3);
@@ -872,11 +862,11 @@ async function api3(path, opts) {
872
862
  }
873
863
  async function main() {
874
864
  if (cmd === "--version" || cmd === "-v") {
875
- const { readFileSync: readFileSync3 } = await import("node:fs");
865
+ const { readFileSync: readFileSync2 } = await import("node:fs");
876
866
  const { join: join4, dirname } = await import("node:path");
877
867
  const { fileURLToPath } = await import("node:url");
878
868
  try {
879
- const pkg = JSON.parse(readFileSync3(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
869
+ const pkg = JSON.parse(readFileSync2(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
880
870
  console.log(`@aion0/forge v${pkg.version}`);
881
871
  } catch {
882
872
  console.log("forge (version unknown)");
@@ -1193,14 +1183,14 @@ Resume in CLI:`);
1193
1183
  }
1194
1184
  case "tunnel_code":
1195
1185
  case "tcode": {
1196
- const { readFileSync: readFileSync3, existsSync: existsSync4 } = await import("node:fs");
1186
+ const { readFileSync: readFileSync2, existsSync: existsSync4 } = await import("node:fs");
1197
1187
  const { join: join4 } = await import("node:path");
1198
1188
  const { getDataDir: _gdd } = await Promise.resolve().then(() => (init_dirs(), dirs_exports));
1199
1189
  const dataDir2 = _gdd();
1200
1190
  const codeFile = join4(dataDir2, "session-code.json");
1201
1191
  try {
1202
1192
  if (existsSync4(codeFile)) {
1203
- const data = JSON.parse(readFileSync3(codeFile, "utf-8"));
1193
+ const data = JSON.parse(readFileSync2(codeFile, "utf-8"));
1204
1194
  if (data.code) {
1205
1195
  console.log(`Session code: ${data.code}`);
1206
1196
  } else {
@@ -1212,7 +1202,7 @@ Resume in CLI:`);
1212
1202
  } catch {
1213
1203
  }
1214
1204
  try {
1215
- const tunnelState = JSON.parse(readFileSync3(join4(dataDir2, "tunnel-state.json"), "utf-8"));
1205
+ const tunnelState = JSON.parse(readFileSync2(join4(dataDir2, "tunnel-state.json"), "utf-8"));
1216
1206
  if (tunnelState.url) console.log(`Tunnel URL: ${tunnelState.url}`);
1217
1207
  } catch {
1218
1208
  }
@@ -1356,9 +1346,9 @@ ${task.gitDiff.slice(0, 2e3)}`);
1356
1346
  cwd: homedir3()
1357
1347
  });
1358
1348
  try {
1359
- const { readFileSync: readFileSync3 } = await import("node:fs");
1349
+ const { readFileSync: readFileSync2 } = await import("node:fs");
1360
1350
  const globalRoot = execSync2("npm root -g", { encoding: "utf-8", cwd: homedir3() }).trim();
1361
- const pkg = JSON.parse(readFileSync3(join4(globalRoot, "@aion0", "forge", "package.json"), "utf-8"));
1351
+ const pkg = JSON.parse(readFileSync2(join4(globalRoot, "@aion0", "forge", "package.json"), "utf-8"));
1362
1352
  console.log(`[forge] Upgraded to v${pkg.version}. Run: forge server restart`);
1363
1353
  } catch {
1364
1354
  console.log("[forge] Upgraded. Run: forge server restart");
@@ -91,7 +91,7 @@ export function listAgents(): AgentConfig[] {
91
91
  enabled: cfg.enabled !== false,
92
92
  isProfile: true,
93
93
  backendType: 'cli',
94
- model: cfg.model || cfg.models?.task,
94
+ model: cfg.models?.task,
95
95
  skipPermissionsFlag: cfg.skipPermissionsFlag || baseAgent?.skipPermissionsFlag,
96
96
  env: cfg.env,
97
97
  cliType: (baseAgent as any)?.cliType || 'generic',
@@ -117,7 +117,7 @@ export function listAgents(): AgentConfig[] {
117
117
  base: cfg.base,
118
118
  isProfile: true,
119
119
  backendType: 'cli',
120
- model: cfg.model || cfg.models?.task,
120
+ model: cfg.models?.task,
121
121
  skipPermissionsFlag: cfg.skipPermissionsFlag || baseAgent?.skipPermissionsFlag,
122
122
  env: cfg.env,
123
123
  cliType: cfg.cliType || (baseAgent as any)?.cliType || 'generic',
@@ -262,20 +262,19 @@ export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
262
262
  || (cliType === 'codex' ? probeCodexSkipFlag(agentCfg.path || 'codex') : cli.skip)
263
263
  || undefined;
264
264
 
265
- // Resolve env/model: either from this agent's own profile fields, or from linked profile
265
+ // Resolve env/model from this agent's per-use models sub-table.
266
+ // 顶层 model 字段已在 0.10 起被 migrate.ts 清理,这里只读 models.terminal。
266
267
  let env: Record<string, string> | undefined;
267
268
  let model: string | undefined;
268
- if (agentCfg.base || agentCfg.env || agentCfg.model || agentCfg.models) {
269
- // This agent has profile-like config — read env/model directly
269
+ if (agentCfg.base || agentCfg.env || agentCfg.models) {
270
270
  if (agentCfg.env) env = { ...agentCfg.env };
271
- model = agentCfg.model || agentCfg.models?.terminal;
272
- if (model === 'default') model = undefined; // 'default' means no override
271
+ model = agentCfg.models?.terminal;
272
+ if (model === 'default') model = undefined; // 'default' = 不加 --model
273
273
  } else if (agentCfg.profile) {
274
- // Agent links to a separate profile — read from that
275
274
  const profileCfg = settings.agents?.[agentCfg.profile];
276
275
  if (profileCfg) {
277
276
  if (profileCfg.env) env = { ...profileCfg.env };
278
- model = profileCfg.model || profileCfg.models?.terminal;
277
+ model = profileCfg.models?.terminal;
279
278
  if (model === 'default') model = undefined;
280
279
  }
281
280
  }
@@ -74,9 +74,19 @@ export function migrateAgentsFlatten(settings: Settings): boolean {
74
74
  }
75
75
  if (raw.type !== undefined) { delete raw.type; mutated = true; }
76
76
 
77
- // 老嵌套 models.task 扁平 model(如果 model 字段空)
78
- if (!raw.model && raw.models?.task && raw.models.task !== 'default') {
79
- raw.model = raw.models.task;
77
+ // 顶层 model 字段已被 per-use models 子表完整取代(terminal / task /
78
+ // telegram / help / mobile 各自可配)。0.10 之前的迁移逻辑反方向复制了
79
+ // models.task → model,导致 resolveTerminalLaunch 错把"task 模型"
80
+ // 当成"全场景兜底",新建项目终端被强插 --model。
81
+ //
82
+ // 这里反向收尾:把残留的 model 搬回 models.task(只在 task 为空或
83
+ // 'default' 时填,避免覆盖用户更近期的选择),然后彻底删字段。
84
+ if (raw.model !== undefined) {
85
+ if (!raw.models) raw.models = {};
86
+ if ((!raw.models.task || raw.models.task === 'default') && raw.model !== 'default') {
87
+ raw.models.task = raw.model;
88
+ }
89
+ delete raw.model;
80
90
  mutated = true;
81
91
  }
82
92
  }
@@ -116,6 +126,7 @@ function detectsLegacyShape(settings: Settings): boolean {
116
126
  if (cfg.type === 'api') return true;
117
127
  if (cfg.base !== undefined) return true;
118
128
  if (cfg.cliType !== undefined) return true;
129
+ if (cfg.model !== undefined) return true; // 顶层 model 已废,改 models.task
119
130
  // 没有 tool + 是 builtin id → 也算需要迁移(补 tool 字段)
120
131
  }
121
132
  // 内置 agent 没有 tool 字段也需要补
package/lib/dirs.ts CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { homedir } from 'node:os';
11
11
  import { join } from 'node:path';
12
- import { existsSync, mkdirSync, renameSync, copyFileSync, readFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, renameSync, copyFileSync } from 'node:fs';
13
13
 
14
14
  /** Shared config directory — only binaries, fixed at ~/.forge/ */
15
15
  export function getConfigDir(): string {
@@ -85,30 +85,10 @@ export function migrateDataDir(): void {
85
85
  console.log('[forge] Migration complete. Old files kept as backup.');
86
86
  }
87
87
 
88
- /** Claude Code home directory — skills, commands, sessions.
89
- *
90
- * Circular-dep note: dirs settings (settings.ts imports getDataDir).
91
- * Used to do `require('./settings')` here as a lazy hack but that
92
- * fires ReferenceError under ESM concurrent loads, and dirs is on
93
- * the hot path for every task / connector / pipeline node.
94
- *
95
- * Cut the cycle by reading the YAML directly here. No parser needed:
96
- * we only need one field (`claudeHome`), which is plain "key: value".
97
- * Falls back to ~/.claude if anything goes sideways.
98
- */
88
+ /** Claude config directory — skills, commands, sessions, etc. Override
89
+ * via the `CLAUDE_HOME` env var (same name claude CLI uses); defaults
90
+ * to `~/.claude`. The settings.yaml `claudeHome` field was removed in
91
+ * 0.10.10 set the env var if you need a non-default location. */
99
92
  export function getClaudeDir(): string {
100
- if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
101
- try {
102
- const settingsFile = join(getDataDir(), 'settings.yaml');
103
- if (existsSync(settingsFile)) {
104
- const raw = readFileSync(settingsFile, 'utf-8');
105
- // Match a top-level `claudeHome: <value>` line. Tolerates quoted /
106
- // unquoted strings and stray whitespace. Not a full YAML parser
107
- // because we explicitly want to avoid loading the YAML module
108
- // (and settings.ts) from this hot path.
109
- const m = raw.match(/^\s*claudeHome:\s*["']?(.+?)["']?\s*$/m);
110
- if (m?.[1]) return m[1].replace(/^~/, homedir());
111
- }
112
- } catch {}
113
- return join(homedir(), '.claude');
93
+ return process.env.CLAUDE_HOME || join(homedir(), '.claude');
114
94
  }
package/lib/settings.ts CHANGED
@@ -27,10 +27,11 @@ export interface AgentEntry {
27
27
  models?: { terminal?: string; task?: string; telegram?: string; help?: string; mobile?: string };
28
28
  skipPermissionsFlag?: string;
29
29
  requiresTTY?: boolean;
30
- model?: string; // flat model override
31
30
  env?: Record<string, string>; // environment variables injected when spawning CLI
32
31
 
33
32
  // === Deprecated (kept for migration compat; removed in a later release) ===
33
+ /** @deprecated 顶层 model 被 models.terminal/task/... 子表替代,migrate.ts 自动清掉 */
34
+ model?: string;
34
35
  /** @deprecated migrated to `tool` */
35
36
  base?: string;
36
37
  /** @deprecated migrated to `tool` */
@@ -84,7 +85,9 @@ export interface Settings {
84
85
  projectRoots: string[];
85
86
  docRoots: string[];
86
87
  claudePath: string;
87
- claudeHome: string;
88
+ // claudeHome removed in 0.10.10 — was a never-UI-exposed override that
89
+ // mostly served to leak `claudeHome: ""` into every yaml. Set the
90
+ // CLAUDE_HOME env var if you need to point at a non-default ~/.claude.
88
91
  telegramBotToken: string;
89
92
  telegramChatId: string;
90
93
  notifyOnComplete: boolean;
@@ -184,7 +187,11 @@ const defaults: Settings = {
184
187
  projectRoots: [],
185
188
  docRoots: [],
186
189
  claudePath: '',
187
- claudeHome: '',
190
+ // claudeHome intentionally omitted — it's an optional override (most users
191
+ // have claude at ~/.claude). Keeping it as a default empty string used to
192
+ // leak `claudeHome: ""` into every saved settings.yaml, which the dirs.ts
193
+ // regex then mis-parsed as the value `"`. See loadSettings normalizer +
194
+ // dirs.ts:getClaudeDir for the read-side handling.
188
195
  telegramBotToken: '',
189
196
  telegramChatId: '',
190
197
  notifyOnComplete: true,
@@ -279,7 +286,21 @@ export function loadSettings(): Settings {
279
286
  if (!existsSync(SETTINGS_FILE)) return { ...defaults };
280
287
  try {
281
288
  const raw = readFileSync(SETTINGS_FILE, 'utf-8');
282
- const parsed = { ...defaults, ...YAML.parse(raw) };
289
+ const parsed: any = { ...defaults, ...YAML.parse(raw) };
290
+ // claudeHome was removed from the schema in 0.10.10. Drop any leftover
291
+ // value so it stops round-tripping through saveSettings. If a user had
292
+ // set a real override (rare — UI never exposed the field), surface a
293
+ // one-time warning pointing them at the CLAUDE_HOME env var.
294
+ if ('claudeHome' in parsed) {
295
+ const old = parsed.claudeHome;
296
+ delete parsed.claudeHome;
297
+ if (old && old !== '') {
298
+ console.warn(
299
+ `[settings] claudeHome="${old}" is no longer read. ` +
300
+ `If you need to override the claude config dir, set CLAUDE_HOME env var instead.`,
301
+ );
302
+ }
303
+ }
283
304
  // Decrypt top-level secret fields
284
305
  for (const field of SECRET_FIELDS) {
285
306
  if (parsed[field] && isEncrypted(parsed[field])) {
package/lib/skills.ts CHANGED
@@ -8,6 +8,7 @@ import { homedir } from 'node:os';
8
8
  import { getDb } from '../src/core/db/database';
9
9
  import { getDbPath } from '../src/config';
10
10
  import { loadSettings } from './settings';
11
+ import { getClaudeDir } from './dirs';
11
12
 
12
13
  type ItemType = 'skill' | 'command';
13
14
 
@@ -271,8 +272,7 @@ async function downloadFile(url: string): Promise<string> {
271
272
  // ─── Install ─────────────────────────────────────────────────
272
273
 
273
274
  function getClaudeHome(): string {
274
- const settings = loadSettings();
275
- return settings.claudeHome || join(homedir(), '.claude');
275
+ return getClaudeDir();
276
276
  }
277
277
 
278
278
  function getSkillDir(name: string, type: ItemType, projectPath?: string): string {
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
13
13
  import { execSync } from 'node:child_process';
14
14
  import type { WorkspaceAgentConfig, WatchTarget, WatchConfig } from './types';
15
15
  import { appendAgentLog } from './persistence';
16
+ import { getClaudeDir } from '../dirs';
16
17
 
17
18
  // ─── Snapshot types ──────────────────────────────────────
18
19
 
@@ -219,7 +220,10 @@ function detectAgentLogChanges(workspaceId: string, targetAgentId: string, patte
219
220
  const lastSessionFile = new Map<string, string>();
220
221
 
221
222
  function detectSessionChanges(projectPath: string, pattern: string | undefined, prevLineCount: number, contextChars = 500, sessionId?: string): { changes: WatchChange | null; lineCount: number } {
222
- const claudeHome = join(homedir(), '.claude', 'projects');
223
+ // Honor CLAUDE_HOME env var so watch-manager looks in the same place as
224
+ // session detection, skills installer, etc. Was previously hardcoded to
225
+ // ~/.claude, which silently broke for users with a custom claude dir.
226
+ const claudeHome = join(getClaudeDir(), 'projects');
223
227
  const encoded = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
224
228
  const sessionDir = join(claudeHome, encoded);
225
229
  if (!existsSync(sessionDir)) return { changes: null, lineCount: prevLineCount };
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.6",
3
+ "version": "0.10.12",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -15,7 +15,7 @@ const pass = (msg: string) => console.log(` ✓ ${msg}`);
15
15
 
16
16
  function baseSettings(): Settings {
17
17
  return {
18
- projectRoots: [], docRoots: [], claudePath: '', claudeHome: '',
18
+ projectRoots: [], docRoots: [], claudePath: '',
19
19
  telegramBotToken: '', telegramChatId: '',
20
20
  notifyOnComplete: true, notifyOnFailure: true,
21
21
  tunnelAutoStart: false, telegramTunnelPassword: '',
@@ -114,7 +114,7 @@ function baseSettings(): Settings {
114
114
  const s = baseSettings();
115
115
  s.agents = {
116
116
  claude: { tool: 'claude', enabled: true, path: '' },
117
- 'forti-coder': { tool: 'claude', enabled: true, model: 'sonnet', env: {} },
117
+ 'forti-coder': { tool: 'claude', enabled: true, models: { task: 'sonnet' }, env: {} },
118
118
  };
119
119
  (s as any).apiProfiles = {
120
120
  'forti-api': { provider: 'openai-compatible', model: 'X', apiKey: 'k', enabled: true },