@aion0/forge 0.10.12 → 0.10.17

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.
Files changed (35) hide show
  1. package/RELEASE_NOTES.md +6 -3
  2. package/app/api/public-info/[resource]/route.ts +40 -0
  3. package/components/SettingsModal.tsx +42 -33
  4. package/components/WorkspaceView.tsx +5 -3
  5. package/lib/agents/known-models.ts +75 -0
  6. package/lib/public-info/fetch.ts +116 -0
  7. package/lib/public-info/types.ts +38 -0
  8. package/lib/public-info/use-models-registry.ts +66 -0
  9. package/lib/settings.ts +9 -0
  10. package/next-env.d.ts +1 -1
  11. package/package.json +1 -1
  12. package/lib/__tests__/foreach-batch-yaml.test.ts +0 -33
  13. package/lib/__tests__/foreach-before.test.ts +0 -201
  14. package/lib/__tests__/foreach-parse.test.ts +0 -114
  15. package/lib/__tests__/foreach-snapshot.test.ts +0 -112
  16. package/lib/__tests__/foreach-source.test.ts +0 -105
  17. package/lib/__tests__/foreach-template.test.ts +0 -112
  18. package/lib/workspace/__tests__/state-machine.test.ts +0 -388
  19. package/lib/workspace/__tests__/workspace.test.ts +0 -311
  20. package/scripts/bench/README.md +0 -66
  21. package/scripts/bench/results/.gitignore +0 -2
  22. package/scripts/bench/run.ts +0 -635
  23. package/scripts/bench/tasks/01-text-utils/task.md +0 -26
  24. package/scripts/bench/tasks/01-text-utils/validator.sh +0 -46
  25. package/scripts/bench/tasks/02-pagination/setup.sh +0 -19
  26. package/scripts/bench/tasks/02-pagination/task.md +0 -48
  27. package/scripts/bench/tasks/02-pagination/validator.sh +0 -69
  28. package/scripts/bench/tasks/03-bug-fix/setup.sh +0 -82
  29. package/scripts/bench/tasks/03-bug-fix/task.md +0 -30
  30. package/scripts/bench/tasks/03-bug-fix/validator.sh +0 -29
  31. package/scripts/test-agents-migrate.ts +0 -149
  32. package/scripts/test-mantis.ts +0 -223
  33. package/scripts/test-memory-local.ts +0 -139
  34. package/scripts/test-memory-upsert.ts +0 -106
  35. package/scripts/verify-usage.ts +0 -178
package/RELEASE_NOTES.md CHANGED
@@ -1,8 +1,11 @@
1
- # Forge v0.10.12
1
+ # Forge v0.10.17
2
2
 
3
3
  Released: 2026-05-30
4
4
 
5
- ## Changes since v0.10.11
5
+ ## Changes since v0.10.16
6
6
 
7
+ ### Features
8
+ - feat: API profile model picker uses registry providers
7
9
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.11...v0.10.12
10
+
11
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.16...v0.10.17
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GET /api/public-info/<resource>?refresh=1
3
+ *
4
+ * Server-side proxy + cache for files under the public-info repo. UI
5
+ * never hits GitHub directly — avoids CORS / rate-limit headaches and
6
+ * gives us a single cache shared across all browser tabs.
7
+ *
8
+ * Supported resources (extend by adding to RESOURCE_MAP):
9
+ * - `models` → `models/registry.json`
10
+ *
11
+ * The UI passes the short name (`models`), this route translates to the
12
+ * actual path under the repo and pairs it with the right fallback.
13
+ */
14
+
15
+ import { NextRequest, NextResponse } from 'next/server';
16
+ import { fetchPublicInfo } from '@/lib/public-info/fetch';
17
+ import { KNOWN_MODELS_FALLBACK } from '@/lib/agents/known-models';
18
+
19
+ interface ResourceSpec {
20
+ path: string;
21
+ fallback: unknown;
22
+ }
23
+
24
+ const RESOURCE_MAP: Record<string, ResourceSpec> = {
25
+ models: { path: 'models/registry.json', fallback: KNOWN_MODELS_FALLBACK },
26
+ };
27
+
28
+ export async function GET(
29
+ req: NextRequest,
30
+ { params }: { params: Promise<{ resource: string }> },
31
+ ): Promise<NextResponse> {
32
+ const { resource } = await params;
33
+ const spec = RESOURCE_MAP[resource];
34
+ if (!spec) {
35
+ return NextResponse.json({ error: `unknown resource: ${resource}` }, { status: 404 });
36
+ }
37
+ const forceRefresh = req.nextUrl.searchParams.get('refresh') === '1';
38
+ const data = await fetchPublicInfo(spec.path, spec.fallback, { forceRefresh });
39
+ return NextResponse.json(data);
40
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { FolderPicker } from './FolderPicker';
5
+ import { useModelsRegistry, pickerOptions } from '@/lib/public-info/use-models-registry';
5
6
 
6
7
  function SecretInput({ value, onChange, placeholder, className }: {
7
8
  value: string;
@@ -917,6 +918,7 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete, isApi: isApiProp
917
918
  onUpdate: (cfg: any) => void; onDelete: () => void;
918
919
  isApi?: boolean;
919
920
  }) {
921
+ const { registry: modelsRegistry } = useModelsRegistry();
920
922
  const [expanded, setExpanded] = useState(false);
921
923
  const [testing, setTesting] = useState(false);
922
924
  const [testResult, setTestResult] = useState<{ ok: boolean; message?: string; error?: string; duration_ms?: number } | null>(null);
@@ -975,20 +977,23 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete, isApi: isApiProp
975
977
  <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
976
978
  <input value={cfg.model || ''} onChange={e => onUpdate({ ...cfg, model: e.target.value })}
977
979
  list={`profile-model-${id}`} className={inputClass} />
978
- {(cfg.cliType === 'claude-code' || (!cfg.cliType && !cfg.base && !isApi)) && (
979
- <datalist id={`profile-model-${id}`}>
980
- <option value="claude-opus-4-6" />
981
- <option value="claude-sonnet-4-6" />
982
- <option value="claude-haiku-4-5-20251001" />
983
- </datalist>
984
- )}
985
- {(cfg.cliType === 'codex' || cfg.base === 'codex') && (
986
- <datalist id={`profile-model-${id}`}>
987
- <option value="codex-mini" />
988
- <option value="o4-mini" />
989
- <option value="gpt-4o" />
990
- </datalist>
991
- )}
980
+ {/* Datalist sourced from forge-public-info registry adding a new
981
+ model id over there shows up here within 24h, no forge release.
982
+ API profiles look up by provider; CLI profiles look up by cliType. */}
983
+ <datalist id={`profile-model-${id}`}>
984
+ {(() => {
985
+ const models = isApi
986
+ ? modelsRegistry.providers?.[cfg.provider || 'anthropic']?.models
987
+ : modelsRegistry.agents[
988
+ cfg.cliType === 'codex' || cfg.base === 'codex' ? 'codex'
989
+ : cfg.cliType === 'aider' || cfg.base === 'aider' ? 'aider'
990
+ : 'claude-code'
991
+ ]?.models;
992
+ return (models || []).map(m => (
993
+ <option key={m.id} value={m.id} />
994
+ ));
995
+ })()}
996
+ </datalist>
992
997
  </div>
993
998
  </div>
994
999
  {isApi ? (
@@ -1127,6 +1132,7 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
1127
1132
  baseAgents: AgentEntry[];
1128
1133
  onAdd: (id: string, cfg: any) => void;
1129
1134
  }) {
1135
+ const { registry: modelsRegistry } = useModelsRegistry();
1130
1136
  const [open, setOpen] = useState(false);
1131
1137
  const [id, setId] = useState('');
1132
1138
  const [name, setName] = useState('');
@@ -1228,22 +1234,17 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
1228
1234
  <div className="flex-1">
1229
1235
  <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
1230
1236
  <input value={model} onChange={e => setModel(e.target.value)}
1231
- placeholder={base === 'claude' ? 'claude-sonnet-4-6' : base === 'codex' ? 'codex-mini' : ''}
1237
+ placeholder={
1238
+ base === 'claude' ? modelsRegistry.agents['claude-code']?.default || ''
1239
+ : base === 'codex' ? modelsRegistry.agents['codex']?.default || ''
1240
+ : ''
1241
+ }
1232
1242
  list={`model-list-${base}`} className={inputClass} />
1233
- {base === 'claude' && (
1234
- <datalist id="model-list-claude">
1235
- <option value="claude-opus-4-6" />
1236
- <option value="claude-sonnet-4-6" />
1237
- <option value="claude-haiku-4-5-20251001" />
1238
- </datalist>
1239
- )}
1240
- {base === 'codex' && (
1241
- <datalist id="model-list-codex">
1242
- <option value="codex-mini" />
1243
- <option value="o4-mini" />
1244
- <option value="gpt-4o" />
1245
- </datalist>
1246
- )}
1243
+ <datalist id={`model-list-${base}`}>
1244
+ {(modelsRegistry.agents[base === 'claude' ? 'claude-code' : base]?.models || []).map(m => (
1245
+ <option key={m.id} value={m.id} />
1246
+ ))}
1247
+ </datalist>
1247
1248
  </div>
1248
1249
  </div>
1249
1250
  <div>
@@ -1275,7 +1276,14 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
1275
1276
  </div>
1276
1277
  <div className="flex-1">
1277
1278
  <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
1278
- <input value={model} onChange={e => setModel(e.target.value)} placeholder="claude-sonnet-4-6" className={inputClass} />
1279
+ <input value={model} onChange={e => setModel(e.target.value)}
1280
+ placeholder={modelsRegistry.providers?.[provider]?.default || ''}
1281
+ list={`api-model-list-${provider}`} className={inputClass} />
1282
+ <datalist id={`api-model-list-${provider}`}>
1283
+ {(modelsRegistry.providers?.[provider]?.models || []).map(m => (
1284
+ <option key={m.id} value={m.id} />
1285
+ ))}
1286
+ </datalist>
1279
1287
  </div>
1280
1288
  </div>
1281
1289
  <div>
@@ -1314,6 +1322,7 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
1314
1322
  }
1315
1323
 
1316
1324
  function AgentsSection({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
1325
+ const { registry: modelsRegistry } = useModelsRegistry();
1317
1326
  const [agents, setAgents] = useState<AgentEntry[]>([]);
1318
1327
  const [loading, setLoading] = useState(true);
1319
1328
  const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
@@ -1649,9 +1658,9 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
1649
1658
  <span className="text-[8px] text-[var(--text-secondary)]">Presets:</span>
1650
1659
  {((() => {
1651
1660
  const ct = (settings.agents?.[a.id] as any)?.cliType || (a.id === 'claude' ? 'claude-code' : a.id === 'codex' ? 'codex' : a.id === 'aider' ? 'aider' : 'generic');
1652
- if (ct === 'claude-code') return ['default', 'sonnet', 'opus', 'haiku', 'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'];
1653
- if (ct === 'codex') return ['default', 'o3-mini', 'o4-mini', 'gpt-4.1'];
1654
- return ['default'];
1661
+ // Models pulled from forge-public-info repo (lib/public-info/fetch.ts).
1662
+ // Add new IDs there and they show up here within 24h with no forge release.
1663
+ return pickerOptions(modelsRegistry, ct);
1655
1664
  })()).map(preset => (
1656
1665
  <button
1657
1666
  key={preset}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useMemo, useRef, forwardRef, useImperativeHandle, lazy, Suspense } from 'react';
4
4
  import { TerminalSessionPickerLazy, fetchAgentSessions, type PickerSelection } from './TerminalLauncher';
5
+ import { useModelsRegistry } from '@/lib/public-info/use-models-registry';
5
6
  import {
6
7
  ReactFlow, Background, Controls, Handle, Position, useReactFlow, ReactFlowProvider,
7
8
  type Node, type NodeProps, MarkerType, type NodeChange,
@@ -715,6 +716,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
715
716
  onConfirm: (cfg: Omit<AgentConfig, 'id'>) => void;
716
717
  onCancel: () => void;
717
718
  }) {
719
+ const { registry: modelsRegistry } = useModelsRegistry();
718
720
  const [label, setLabel] = useState(initial.label || '');
719
721
  const [icon, setIcon] = useState(initial.icon || '🤖');
720
722
  const [role, setRole] = useState(initial.role || '');
@@ -1160,9 +1162,9 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
1160
1162
  list="workspace-model-list"
1161
1163
  className="text-xs bg-[#161b22] border border-[#30363d] rounded px-2 py-1 text-white focus:outline-none focus:border-[#58a6ff] font-mono" />
1162
1164
  <datalist id="workspace-model-list">
1163
- <option value="claude-sonnet-4-6" />
1164
- <option value="claude-opus-4-6" />
1165
- <option value="claude-haiku-4-5-20251001" />
1165
+ {(modelsRegistry.agents['claude-code']?.models || []).map(m => (
1166
+ <option key={m.id} value={m.id} />
1167
+ ))}
1166
1168
  </datalist>
1167
1169
  </div>
1168
1170
  );
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Built-in fallback model list. Forge prefers the public-info repo's
3
+ * registry (see `lib/public-info/fetch.ts`), but falls back to this
4
+ * when the network is unreachable, the file is malformed, or the
5
+ * user is on first-run before the cache is populated.
6
+ *
7
+ * Keep this list reasonably current — if the registry is permanently
8
+ * unreachable, this is what users see. New models in the wild should
9
+ * land in the public-info repo first (no code change required), and
10
+ * trickle into this fallback whenever the next forge release happens.
11
+ */
12
+
13
+ import type { ModelsRegistry } from '../public-info/types';
14
+
15
+ export const KNOWN_MODELS_FALLBACK: ModelsRegistry = {
16
+ version: 1,
17
+ updatedAt: '2026-05-30',
18
+ note: 'Bundled fallback — actual current list lives in forge-public-info/models/registry.json',
19
+ agents: {
20
+ 'claude-code': {
21
+ displayName: 'Claude Code',
22
+ default: 'claude-sonnet-4-6',
23
+ aliases: [
24
+ { id: 'default', label: 'default (CLI decides)' },
25
+ { id: 'sonnet', label: 'sonnet (alias)' },
26
+ { id: 'opus', label: 'opus (alias)' },
27
+ { id: 'haiku', label: 'haiku (alias)' },
28
+ ],
29
+ models: [
30
+ { id: 'claude-opus-4-8', label: 'Opus 4.8', tier: 'premium' },
31
+ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', tier: 'standard', default: true },
32
+ { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', tier: 'fast' },
33
+ ],
34
+ },
35
+ codex: {
36
+ displayName: 'OpenAI Codex',
37
+ default: 'o4-mini',
38
+ aliases: [{ id: 'default', label: 'default (CLI decides)' }],
39
+ models: [
40
+ { id: 'o4-mini', label: 'o4-mini', tier: 'fast', default: true },
41
+ { id: 'o3-mini', label: 'o3-mini', tier: 'fast' },
42
+ { id: 'gpt-4.1', label: 'GPT-4.1', tier: 'standard' },
43
+ ],
44
+ },
45
+ aider: {
46
+ displayName: 'Aider',
47
+ default: 'default',
48
+ aliases: [{ id: 'default', label: 'default (CLI decides)' }],
49
+ models: [],
50
+ },
51
+ },
52
+ providers: {
53
+ anthropic: {
54
+ displayName: 'Anthropic API',
55
+ default: 'claude-sonnet-4-6',
56
+ aliases: [],
57
+ models: [
58
+ { id: 'claude-opus-4-8', label: 'Opus 4.8', tier: 'premium' },
59
+ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', tier: 'standard', default: true },
60
+ { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', tier: 'fast' },
61
+ ],
62
+ },
63
+ openai: {
64
+ displayName: 'OpenAI API',
65
+ default: 'gpt-4.1',
66
+ aliases: [],
67
+ models: [
68
+ { id: 'gpt-4.1', label: 'GPT-4.1', tier: 'premium', default: true },
69
+ { id: 'gpt-4o', label: 'GPT-4o', tier: 'standard' },
70
+ { id: 'o4-mini', label: 'o4-mini', tier: 'fast' },
71
+ { id: 'o3-mini', label: 'o3-mini', tier: 'fast' },
72
+ ],
73
+ },
74
+ },
75
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Generic fetch+cache helper for `forge-public-info` resources.
3
+ *
4
+ * Pattern: GitHub-hosted JSON files live under
5
+ * `<publicInfoRepoUrl>/<resourceDir>/<file>.json`
6
+ *
7
+ * On read:
8
+ * 1. If a fresh cached copy exists (< TTL), return it.
9
+ * 2. Otherwise fetch from the configured `publicInfoRepoUrl`.
10
+ * 3. On any error (offline, 4xx, malformed JSON), return the bundled
11
+ * fallback so the UI never breaks.
12
+ *
13
+ * Cache lives at `<dataDir>/public-info-cache/<resource>.json`. Each
14
+ * file has a sidecar `<resource>.meta.json` carrying the fetch
15
+ * timestamp; we don't trust HTTP `Last-Modified` because GitHub raw
16
+ * serves through a CDN that strips it.
17
+ */
18
+
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { getDataDir } from '../dirs';
22
+ import { loadSettings } from '../settings';
23
+
24
+ const DEFAULT_REPO_URL = 'https://raw.githubusercontent.com/aiwatching/forge-public-info/main';
25
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
26
+ const FETCH_TIMEOUT_MS = 5000;
27
+
28
+ interface CacheMeta { fetchedAt: number; etag?: string }
29
+
30
+ function cacheDir(): string {
31
+ const dir = join(getDataDir(), 'public-info-cache');
32
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
33
+ return dir;
34
+ }
35
+
36
+ function repoUrl(): string {
37
+ return (loadSettings() as any).publicInfoRepoUrl || DEFAULT_REPO_URL;
38
+ }
39
+
40
+ function cacheKey(resourcePath: string): string {
41
+ // public/info/models/registry.json → models-registry
42
+ return resourcePath.replace(/\.json$/, '').replace(/[^a-zA-Z0-9]/g, '-').replace(/^-+|-+$/g, '');
43
+ }
44
+
45
+ function readCache(resourcePath: string): { data: unknown; meta: CacheMeta } | null {
46
+ const key = cacheKey(resourcePath);
47
+ const dataPath = join(cacheDir(), `${key}.json`);
48
+ const metaPath = join(cacheDir(), `${key}.meta.json`);
49
+ if (!existsSync(dataPath) || !existsSync(metaPath)) return null;
50
+ try {
51
+ return {
52
+ data: JSON.parse(readFileSync(dataPath, 'utf-8')),
53
+ meta: JSON.parse(readFileSync(metaPath, 'utf-8')),
54
+ };
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function writeCache(resourcePath: string, data: unknown): void {
61
+ const key = cacheKey(resourcePath);
62
+ try {
63
+ writeFileSync(join(cacheDir(), `${key}.json`), JSON.stringify(data, null, 2), 'utf-8');
64
+ writeFileSync(
65
+ join(cacheDir(), `${key}.meta.json`),
66
+ JSON.stringify({ fetchedAt: Date.now() } satisfies CacheMeta, null, 2),
67
+ 'utf-8',
68
+ );
69
+ } catch (err) {
70
+ console.warn('[public-info] cache write failed:', err instanceof Error ? err.message : err);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Fetch a public-info resource with built-in cache + fallback.
76
+ *
77
+ * @param resourcePath Path under the repo, e.g. `"models/registry.json"`
78
+ * @param fallback Used when network + cache both fail
79
+ * @param opts.forceRefresh Skip cache TTL check (e.g., user-triggered refresh)
80
+ */
81
+ export async function fetchPublicInfo<T>(
82
+ resourcePath: string,
83
+ fallback: T,
84
+ opts: { forceRefresh?: boolean } = {},
85
+ ): Promise<T> {
86
+ const now = Date.now();
87
+
88
+ // 1. Cache hit (fresh)
89
+ if (!opts.forceRefresh) {
90
+ const cached = readCache(resourcePath);
91
+ if (cached && now - cached.meta.fetchedAt < CACHE_TTL_MS) {
92
+ return cached.data as T;
93
+ }
94
+ }
95
+
96
+ // 2. Network fetch
97
+ const url = `${repoUrl()}/${resourcePath}`;
98
+ try {
99
+ const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
100
+ if (res.ok) {
101
+ const data = await res.json();
102
+ writeCache(resourcePath, data);
103
+ return data as T;
104
+ }
105
+ console.warn(`[public-info] ${url} returned HTTP ${res.status}, using fallback`);
106
+ } catch (err) {
107
+ console.warn(`[public-info] ${url} fetch failed:`, err instanceof Error ? err.message : err);
108
+ }
109
+
110
+ // 3. Stale cache (better than fallback)
111
+ const cached = readCache(resourcePath);
112
+ if (cached) return cached.data as T;
113
+
114
+ // 4. Bundled fallback
115
+ return fallback;
116
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shapes of JSON resources served from the public-info repo
3
+ * (https://github.com/aiwatching/forge-public-info, by default).
4
+ *
5
+ * Each resource lives under `<repoUrl>/<resource-dir>/<file>.json` so new
6
+ * resource types can be added without touching settings. Forge fetches
7
+ * them via `lib/public-info/fetch.ts` (24h cache + built-in fallback).
8
+ */
9
+
10
+ export interface ModelInfo {
11
+ id: string;
12
+ label: string;
13
+ /** Rough quality/cost band — used by UI to sort/group. */
14
+ tier?: 'premium' | 'standard' | 'fast';
15
+ default?: boolean;
16
+ }
17
+
18
+ export interface ModelAlias {
19
+ id: string;
20
+ label: string;
21
+ }
22
+
23
+ export interface AgentModels {
24
+ displayName: string;
25
+ default: string;
26
+ aliases: ModelAlias[];
27
+ models: ModelInfo[];
28
+ }
29
+
30
+ export interface ModelsRegistry {
31
+ version: number;
32
+ updatedAt: string;
33
+ note?: string;
34
+ /** Keyed by CLI type (e.g. `claude-code`, `codex`, `aider`). */
35
+ agents: Record<string, AgentModels>;
36
+ /** Keyed by API provider id (e.g. `anthropic`, `openai`, `grok`, `google`, `deepseek`, `litellm`). */
37
+ providers?: Record<string, AgentModels>;
38
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Client-side hook for the model registry. Lazy-fetches from
3
+ * `GET /api/public-info/models`, caches in module-level state so
4
+ * multiple components sharing the registry hit the network once per
5
+ * page load. Returns the bundled fallback shape until the network
6
+ * round-trip completes so callers never see `undefined`.
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import { useEffect, useState } from 'react';
12
+ import { KNOWN_MODELS_FALLBACK } from '../agents/known-models';
13
+ import type { ModelsRegistry, AgentModels } from './types';
14
+
15
+ let cached: ModelsRegistry | null = null;
16
+ let inflight: Promise<ModelsRegistry> | null = null;
17
+
18
+ async function load(refresh = false): Promise<ModelsRegistry> {
19
+ if (cached && !refresh) return cached;
20
+ if (inflight && !refresh) return inflight;
21
+ inflight = (async () => {
22
+ try {
23
+ const res = await fetch(`/api/public-info/models${refresh ? '?refresh=1' : ''}`);
24
+ if (res.ok) {
25
+ const data = await res.json();
26
+ cached = data as ModelsRegistry;
27
+ return cached;
28
+ }
29
+ } catch {}
30
+ cached = KNOWN_MODELS_FALLBACK;
31
+ return cached;
32
+ })();
33
+ try { return await inflight; } finally { inflight = null; }
34
+ }
35
+
36
+ export function useModelsRegistry(): {
37
+ registry: ModelsRegistry;
38
+ refresh: () => Promise<void>;
39
+ loading: boolean;
40
+ } {
41
+ const [registry, setRegistry] = useState<ModelsRegistry>(cached ?? KNOWN_MODELS_FALLBACK);
42
+ const [loading, setLoading] = useState(!cached);
43
+
44
+ useEffect(() => {
45
+ if (cached) return;
46
+ setLoading(true);
47
+ load().then(r => { setRegistry(r); setLoading(false); });
48
+ }, []);
49
+
50
+ async function refresh() {
51
+ setLoading(true);
52
+ const r = await load(true);
53
+ setRegistry(r);
54
+ setLoading(false);
55
+ }
56
+
57
+ return { registry, refresh, loading };
58
+ }
59
+
60
+ /** Get the model option list for a specific agent CLI type. Returns the
61
+ * flat list of `[...aliases, ...models]` as the picker expects them. */
62
+ export function pickerOptions(registry: ModelsRegistry, cliType: string): string[] {
63
+ const agent: AgentModels | undefined = registry.agents[cliType];
64
+ if (!agent) return ['default'];
65
+ return [...agent.aliases.map(a => a.id), ...agent.models.map(m => m.id)];
66
+ }
package/lib/settings.ts CHANGED
@@ -112,6 +112,14 @@ export interface Settings {
112
112
  * shape as connectorsRepoUrl. Default: `aiwatching/forge-workflow`.
113
113
  */
114
114
  workflowRepoUrl: string;
115
+ /**
116
+ * Base URL for the `forge-public-info` repo. Houses model lists and
117
+ * other small JSON files that the maintainer updates between npm
118
+ * releases — push a JSON commit, all users pick it up within 24h via
119
+ * the cache. Sub-paths (e.g. `models/registry.json`) are appended by
120
+ * `lib/public-info/fetch.ts`. Default: `aiwatching/forge-public-info`.
121
+ */
122
+ publicInfoRepoUrl: string;
115
123
  /**
116
124
  * Maximum concurrent pipeline runs (running + pending). When a Job's
117
125
  * scheduler tick would push the total above this, additional items
@@ -207,6 +215,7 @@ const defaults: Settings = {
207
215
  skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
208
216
  connectorsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-connectors/main',
209
217
  workflowRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-workflow/main',
218
+ publicInfoRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-public-info/main',
210
219
  maxConcurrentPipelines: 5,
211
220
  displayName: 'Forge',
212
221
  displayEmail: '',
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/dev/types/routes.d.ts";
3
+ import "./.next/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.12",
3
+ "version": "0.10.17",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,33 +0,0 @@
1
- /**
2
- * Smoke test: fortinet-mr-review-batch v0.3.0 parses end-to-end through
3
- * parseWorkflow with the new for_each.before extension.
4
- */
5
- import { parseWorkflow } from '../pipeline';
6
- import { readFileSync } from 'fs';
7
-
8
- const txt = readFileSync('/Users/zliu/.forge/data/flows/fortinet-mr-review-batch.yaml', 'utf8');
9
- const wf = parseWorkflow(txt);
10
-
11
- let ok = true;
12
- function eq(actual: any, expected: any, label: string) {
13
- if (JSON.stringify(actual) === JSON.stringify(expected)) {
14
- console.log(` ✓ ${label}`);
15
- } else {
16
- console.log(` ✗ ${label}: got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)}`);
17
- ok = false;
18
- }
19
- }
20
-
21
- console.log('fortinet-mr-review-batch v0.3.0 — parseWorkflow integration');
22
- eq(wf.name, 'fortinet-mr-review-batch', 'workflow name');
23
- eq(wf.for_each?.source, '{{nodes.list-iids.outputs.iids}}', 'for_each.source');
24
- eq(wf.for_each?.as, 'mr_iid', 'for_each.as');
25
- eq(wf.for_each?.on_failure, 'continue', 'for_each.on_failure');
26
- eq(wf.for_each?.before, ['list-iids'], 'for_each.before');
27
- eq(Object.keys(wf.nodes), ['list-iids', 'ingest', 'triage', 'fix', 'reply', 'cleanup'], 'all 6 nodes parsed');
28
- eq(wf.nodes['list-iids']?.mode, 'shell', 'list-iids mode = shell');
29
- eq(wf.nodes['list-iids']?.worktree, false, 'list-iids worktree = false');
30
- eq(wf.nodes['list-iids']?.outputs?.[0]?.name, 'iids', 'list-iids outputs.iids name');
31
- eq(wf.nodes['list-iids']?.outputs?.[0]?.extract, 'stdout', 'list-iids outputs.iids extract');
32
-
33
- process.exit(ok ? 0 : 1);