@aion0/forge 0.10.36 → 0.10.38

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.
@@ -831,6 +831,31 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
831
831
  />
832
832
  </div>
833
833
 
834
+ {/* Re-run Onboarding */}
835
+ <div className="space-y-2">
836
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
837
+ Onboarding
838
+ </label>
839
+ <button
840
+ type="button"
841
+ onClick={async () => {
842
+ await fetch('/api/onboarding', {
843
+ method: 'POST',
844
+ headers: { 'Content-Type': 'application/json' },
845
+ body: JSON.stringify({ action: 'reset' }),
846
+ });
847
+ // Reload so the Dashboard banner re-renders.
848
+ window.location.reload();
849
+ }}
850
+ className="text-[11px] px-2.5 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white"
851
+ >
852
+ ↺ Re-run Onboarding wizard
853
+ </button>
854
+ <p className="text-[10px] text-[var(--text-secondary)]">
855
+ Re-opens the first-run setup banner. Existing values are preserved — the wizard never overwrites non-empty fields.
856
+ </p>
857
+ </div>
858
+
834
859
  {/* Admin Password */}
835
860
  <div className="space-y-2">
836
861
  <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
@@ -1041,6 +1066,23 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete, isApi: isApiProp
1041
1066
  className={inputClass + ' font-mono'}
1042
1067
  />
1043
1068
  </div>
1069
+ <div>
1070
+ <label className="text-[8px] text-[var(--text-secondary)]">
1071
+ Max input tokens — total prompt cap (system + memory + history + tools). Default 200000 (Claude Code working window). Anything beyond gets strictly compressed: memory tail dropped first, then oldest history. Lower it (e.g. 12000) for 16k-context legacy models.
1072
+ </label>
1073
+ <input
1074
+ type="number"
1075
+ min={4000}
1076
+ value={cfg.maxInputTokens ?? ''}
1077
+ onChange={e => {
1078
+ const raw = e.target.value.trim();
1079
+ const n = raw === '' ? undefined : Number(raw);
1080
+ onUpdate({ ...cfg, maxInputTokens: Number.isFinite(n) ? n : undefined });
1081
+ }}
1082
+ placeholder="200000 (default — Claude Code 200k)"
1083
+ className={inputClass + ' font-mono'}
1084
+ />
1085
+ </div>
1044
1086
  <div className="flex items-center gap-2 pt-1">
1045
1087
  <button
1046
1088
  onClick={runTest}
@@ -268,6 +268,16 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
268
268
  const [expandedRoot, setExpandedRoot] = useState<string | null>(null);
269
269
  const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string; detected?: boolean }[]>([]);
270
270
  const [selectedAgent, setSelectedAgent] = useState<string>('');
271
+ // Cached default-agent cliCmd. Resolved once on mount (fast in theory but
272
+ // the route can take 1–5s when many concurrent fetches saturate workers).
273
+ // openSessionInTerminal reads this directly — no await per session restore.
274
+ const defaultAgentCmdRef = useRef<string>('claude');
275
+ useEffect(() => {
276
+ fetch('/api/agents?resolve=')
277
+ .then(r => r.json())
278
+ .then(info => { if (info?.cliCmd) defaultAgentCmdRef.current = info.cliCmd; })
279
+ .catch(() => { /* keep bare 'claude' */ });
280
+ }, []);
271
281
  const [defaultAgentId, setDefaultAgentId] = useState('claude');
272
282
 
273
283
  // Restore shared state from server after mount
@@ -359,7 +369,12 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
359
369
  const sf = skipPermissions ? ' --dangerously-skip-permissions' : '';
360
370
  let mcpFlag = '';
361
371
  try { const { getMcpFlag } = await import('@/lib/session-utils'); mcpFlag = await getMcpFlag(projectPath); } catch {}
362
- const cmd = `cd "${projectPath}" && claude --resume ${sessionId}${sf}${mcpFlag}\n`;
372
+ // Use the cached default-agent cliCmd. Pre-fetched on mount above;
373
+ // if still 'claude' (fetch slow / pending), we use bare claude — old
374
+ // behavior, wrong if conda-base shadows the real one, but at least
375
+ // doesn't block the command from appearing.
376
+ const agentCmd = defaultAgentCmdRef.current;
377
+ const cmd = `cd "${projectPath}" && ${agentCmd} --resume ${sessionId}${sf}${mcpFlag}\n`;
363
378
  pendingCommands.set(paneId, cmd);
364
379
  const projectName = projectPath.split('/').pop() || 'Terminal';
365
380
  const newTab: TabState = {
@@ -47,15 +47,25 @@ import type {
47
47
  // sentinel message below so the user knows why the turn stopped.
48
48
  const MAX_ITERATIONS = 24;
49
49
  const MAX_TOKENS = 16000;
50
- // Working-window budgets for the LLM history. Capped by message count
51
- // AND by token estimate (whichever hits first), see design §8. Older
52
- // raw is summarized by the memory-standalone Temper Summary sub-task
53
- // and recalled via buildMemoryContext as compact blocks instead.
50
+ // Working-window message-count cap. Token cap is dynamic per-profile
51
+ // (see DEFAULT_MAX_INPUT_TOKENS + ApiProfile.maxInputTokens). Older raw
52
+ // is summarized by the memory-standalone Temper Summary sub-task and
53
+ // recalled via buildMemoryContext as compact blocks.
54
54
  const HISTORY_MSG_BUDGET = 60;
55
- // Bumped 8000 32000 modern models all 200k context; 8000 was
56
- // stripping single oversized tool results (e.g. mantis.search_bugs
57
- // returning 20k chars), leaving history empty after orphan-trim.
58
- const HISTORY_TOKEN_BUDGET = 32000;
55
+ // Default total input ceiling when the profile doesn't set one. Matches
56
+ // Claude Code's 200k working window modern Claude / DeepSeek-V3 /
57
+ // Qwen-Max all expose 200k context. Anything beyond this gets strictly
58
+ // compressed (memory tail trimmed first, then oldest history evicted).
59
+ // Lower it per-profile (e.g. 12000) for older 16k-context models —
60
+ // Forge auto-trims memory then history to fit.
61
+ const DEFAULT_MAX_INPUT_TOKENS = 200_000;
62
+ // Reserved for the model's reply so we don't have to fight Anthropic
63
+ // hard limits where max_tokens is part of the context budget.
64
+ const OUTPUT_RESERVE_TOKENS = MAX_TOKENS;
65
+ // Absolute floor — if even after trimming memory we can't fit this
66
+ // many tokens of history, the profile's ceiling is unworkable and we
67
+ // surface a clear error instead of sending a useless request.
68
+ const MIN_HISTORY_TOKENS = 2_000;
59
69
  // Hard cap on a single tool_result stored into the conversation (chars).
60
70
  // A giant result (e.g. a connector returning a full test tree) would
61
71
  // otherwise blow the whole HISTORY_TOKEN_BUDGET, push its paired
@@ -112,6 +122,9 @@ export interface ProviderResolution {
112
122
  apiKey: string;
113
123
  baseUrl: string;
114
124
  model: string;
125
+ /** Total input-token ceiling for chat (system + memory + history + tools).
126
+ * Falls back to DEFAULT_MAX_INPUT_TOKENS when the profile leaves it unset. */
127
+ maxInputTokens?: number;
115
128
  }
116
129
 
117
130
  /**
@@ -204,6 +217,7 @@ export function resolveProvider(sessionProvider: string | null, sessionModel: st
204
217
  apiKey: profile.apiKey,
205
218
  baseUrl: profile.baseUrl || defaultBaseUrl(profile.provider),
206
219
  model,
220
+ maxInputTokens: profile.maxInputTokens,
207
221
  };
208
222
  }
209
223
 
@@ -607,8 +621,11 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
607
621
  let openConnectorTools = allConnectorTools.filter((t) => openSet.has(t.name.split('.')[0]!));
608
622
  let allTools: LlmTool[] = [...builtinToolDefs, ...openConnectorTools];
609
623
 
624
+ // Keep memContext separate from `system` — we may need to trim it
625
+ // per-iteration when the profile's maxInputTokens is tight. The
626
+ // assembled string is recomputed each iter (after open-set + memory
627
+ // trim). `system` here holds the base (no memory section).
610
628
  let system = buildSystem(openConnectorTools, openSet);
611
- if (memContext) system += '\n\n─── Memory context (auto-loaded) ───\n' + memContext;
612
629
  if (memStore.enabled) {
613
630
  const searchHint = memStore.kind === 'local'
614
631
  ? '• memory_search is keyword LIKE over local blocks + episodes — useful for finding past notes; prefer memory_get_block / memory_list_blocks for first-person facts.'
@@ -627,23 +644,17 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
627
644
  while (iter < MAX_ITERATIONS) {
628
645
  iter += 1;
629
646
 
630
- const history = trimOrphanToolResults(
631
- listMessagesCapped(args.sessionId, HISTORY_MSG_BUDGET, HISTORY_TOKEN_BUDGET, estimateTokens),
632
- );
633
- // Belt-and-suspenders: tool_result truncation should keep a complete
634
- // pair in-window, but if history is somehow empty, fail clearly
635
- // instead of letting the provider throw "messages must not be empty".
636
- if (history.length === 0) {
637
- cb({ type: 'error', data: { error: 'Conversation context is empty after trimming an oversized result. Clear the chat or retry with a narrower query.' } });
638
- return { ok: false, error: 'empty history' };
639
- }
640
-
641
647
  // ── Recompute open set every iteration ──────────────────────
642
648
  // Scan history (since last user text msg) + this turn's accumulated
643
649
  // blocks → which connectors are open right now. Then filter tools.
644
650
  // First iteration: only user-text auto-opens seed the set. After
645
651
  // the LLM calls connector_open, subsequent iterations pick that up.
646
- const newOpenSet = computeOpenSet(history, assistantBlocksAccum);
652
+ // (Computed off a preview slice of history — refined below once
653
+ // we have the real history under budget.)
654
+ const previewHistory = trimOrphanToolResults(
655
+ listMessagesCapped(args.sessionId, HISTORY_MSG_BUDGET, 8_000, estimateTokens),
656
+ );
657
+ const newOpenSet = computeOpenSet(previewHistory, assistantBlocksAccum);
647
658
  const setChanged = newOpenSet.size !== openSet.size ||
648
659
  [...newOpenSet].some((n) => !openSet.has(n));
649
660
  if (setChanged) {
@@ -651,23 +662,69 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
651
662
  openConnectorTools = allConnectorTools.filter((t) => openSet.has(t.name.split('.')[0]!));
652
663
  allTools = [...builtinToolDefs, ...openConnectorTools];
653
664
  system = buildSystem(openConnectorTools, openSet);
654
- if (memContext) system += '\n\n─── Memory context (auto-loaded) ───\n' + memContext;
655
665
  console.log(`[chat] open set → {${[...openSet].join(',')}} (${openConnectorTools.length} connector tools active)`);
656
666
  }
657
667
 
668
+ // ── Dynamic context budget ──────────────────────────────────
669
+ // Total profile cap (default 60k, override per profile). Subtract
670
+ // fixed contributors (system, tools schema) and reservation for
671
+ // the model's reply → what's left splits between memory + history.
672
+ // Memory gets trimmed (from tail = least-pinned recall hits) when
673
+ // history would dip below the floor.
674
+ const maxInputTokens = provider.maxInputTokens || DEFAULT_MAX_INPUT_TOKENS;
675
+ const systemTok = Math.ceil(system.length / 4);
676
+ const toolsTok = Math.ceil(JSON.stringify(allTools).length / 4);
677
+ const fixedTok = systemTok + toolsTok + OUTPUT_RESERVE_TOKENS;
678
+
679
+ let memCtxTrimmed = memContext;
680
+ let memCtxTok = Math.ceil(memCtxTrimmed.length / 4);
681
+ let historyBudget = maxInputTokens - fixedTok - memCtxTok;
682
+
683
+ if (historyBudget < MIN_HISTORY_TOKENS) {
684
+ // Need to recover from memory. Each token = ~4 chars; cut from the
685
+ // END of memContext (renderMemoryContext emits pinned blocks first,
686
+ // recall hits last → tail is the least-pinned, easiest to drop).
687
+ const shortBy = MIN_HISTORY_TOKENS - historyBudget;
688
+ const charsToDrop = Math.min(memCtxTrimmed.length, shortBy * 4);
689
+ if (charsToDrop > 0) {
690
+ memCtxTrimmed = memCtxTrimmed.slice(0, memCtxTrimmed.length - charsToDrop)
691
+ + (memCtxTrimmed.length - charsToDrop > 0 ? '\n[… memory context trimmed to fit profile.maxInputTokens]' : '');
692
+ memCtxTok = Math.ceil(memCtxTrimmed.length / 4);
693
+ historyBudget = maxInputTokens - fixedTok - memCtxTok;
694
+ }
695
+ }
696
+
697
+ if (historyBudget < 500) {
698
+ cb({ type: 'error', data: { error:
699
+ `API profile maxInputTokens=${maxInputTokens} too small: ` +
700
+ `baseline (system=${systemTok} + tools=${toolsTok} + memory=${memCtxTok} + reserve=${OUTPUT_RESERVE_TOKENS}) ` +
701
+ `already takes ${fixedTok + memCtxTok} tokens. ` +
702
+ `Raise maxInputTokens in Settings → API Profiles, or pick a model with a larger context window.`
703
+ } });
704
+ return { ok: false, error: 'profile context budget exhausted' };
705
+ }
706
+
707
+ const history = trimOrphanToolResults(
708
+ listMessagesCapped(args.sessionId, HISTORY_MSG_BUDGET, historyBudget, estimateTokens),
709
+ );
710
+ if (history.length === 0) {
711
+ cb({ type: 'error', data: { error: 'Conversation context is empty after trimming an oversized result. Clear the chat or retry with a narrower query.' } });
712
+ return { ok: false, error: 'empty history' };
713
+ }
714
+
715
+ // Stitch trimmed memContext onto base system for this call only.
716
+ const systemForCall = memCtxTrimmed
717
+ ? `${system}\n\n─── Memory context (auto-loaded) ───\n${memCtxTrimmed}`
718
+ : system;
719
+
658
720
  assistantBlocksAccum = [];
659
721
  let currentTextBuf = '';
660
722
 
661
723
  // ── Token composition log (input side, BEFORE the call) ──
662
- // Heuristic char/4. Lets you correlate later with the provider's
663
- // real usage.input_tokens — if the gap widens turn-over-turn, the
664
- // memory/tools blob is silently growing.
665
- const _systemTok = Math.ceil(system.length / 4);
666
- const _memCtxTok = Math.ceil(memContext.length / 4);
667
- const _toolsTok = Math.ceil(JSON.stringify(allTools).length / 4);
668
724
  const _historyTok = history.reduce((s, m) => s + estimateTokens(m), 0);
669
725
  const _historyMsgs = history.length;
670
- console.log(`[chat-tokens] session=${args.sessionId} turn=${iter} est_in=${_systemTok + _historyTok + _toolsTok} system=${_systemTok} history=${_historyTok}(${_historyMsgs}msgs) memory=${_memCtxTok} tools=${_toolsTok}`);
726
+ const _est_in = systemTok + memCtxTok + toolsTok + _historyTok;
727
+ console.log(`[chat-tokens] session=${args.sessionId} turn=${iter} cap=${maxInputTokens} est_in=${_est_in} system=${systemTok} memory=${memCtxTok}${memCtxTrimmed.length < memContext.length ? '(trimmed)' : ''} tools=${toolsTok} history=${_historyTok}(${_historyMsgs}msgs) reserve=${OUTPUT_RESERVE_TOKENS}`);
671
728
 
672
729
  const result = await streamLlm(
673
730
  {
@@ -675,7 +732,7 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
675
732
  apiKey: provider.apiKey,
676
733
  baseUrl: provider.baseUrl,
677
734
  model: provider.model,
678
- system,
735
+ system: systemForCall,
679
736
  history,
680
737
  tools: allTools,
681
738
  maxTokens: MAX_TOKENS,
@@ -80,8 +80,12 @@ export const openaiAdapter: LlmAdapter = {
80
80
  // Some providers (litellm/vLLM) reject `tools: []` — they want the
81
81
  // field omitted entirely when there are no tools.
82
82
  const hasTools = Object.keys(tools).length > 0;
83
+ // Force /v1/chat/completions. @ai-sdk/openai v3 routes the default
84
+ // factory to the Responses API (/v1/responses), which only OpenAI
85
+ // first-party hosts — DeepSeek/Qianwen/LiteLLM proxies/vLLM/Ollama
86
+ // all 400 on it. .chat() picks the universally-supported endpoint.
83
87
  const result = streamText({
84
- model: client(req.model),
88
+ model: client.chat(req.model),
85
89
  system: req.system,
86
90
  messages: historyToModelMessages(req.history),
87
91
  ...(hasTools ? { tools } : {}),
@@ -231,6 +231,18 @@ function touchSession(id: string): void {
231
231
 
232
232
  // ─── Messages ────────────────────────────────────────────
233
233
 
234
+ // Cap any single field to 64KB before it hits sqlite. LLM proxies (LiteLLM,
235
+ // vLLM) sometimes return error payloads listing every message that failed
236
+ // validation — a 188-turn chat trips 6000+ entries and the JSON balloons
237
+ // to 25+ MB. One bad row drags out every UI page-load that touches it,
238
+ // so truncate at write time.
239
+ const FIELD_BYTE_CAP = 64 * 1024;
240
+ function capLargeText(s: string | undefined | null): string | null {
241
+ if (s == null) return null;
242
+ if (s.length <= FIELD_BYTE_CAP) return s;
243
+ return s.slice(0, FIELD_BYTE_CAP) + `\n… (truncated, full length ${s.length} chars)`;
244
+ }
245
+
234
246
  export function appendMessage(opts: {
235
247
  session_id: string;
236
248
  role: Role;
@@ -238,13 +250,21 @@ export function appendMessage(opts: {
238
250
  error?: string;
239
251
  }): Message {
240
252
  ensureSchema();
253
+ // Truncate any text block that's gone runaway (e.g. a streamed error
254
+ // message stuffed into the assistant turn for UI display).
255
+ const cappedBlocks = opts.blocks.map(b => {
256
+ if (b.type === 'text' && b.text.length > FIELD_BYTE_CAP) {
257
+ return { ...b, text: capLargeText(b.text)! };
258
+ }
259
+ return b;
260
+ });
241
261
  const row: MessageRow = {
242
262
  id: randomUUID(),
243
263
  session_id: opts.session_id,
244
264
  role: opts.role,
245
- blocks: JSON.stringify(opts.blocks),
265
+ blocks: JSON.stringify(cappedBlocks),
246
266
  ts: Date.now(),
247
- error: opts.error ?? null,
267
+ error: capLargeText(opts.error),
248
268
  };
249
269
  db().prepare(`
250
270
  INSERT INTO chat_messages (id, session_id, role, blocks, ts, error)
@@ -709,7 +709,7 @@ function findConnectorTool(qualified: string): {
709
709
  * the extension's runner finishes those at execution time.
710
710
  */
711
711
  function buildConnectorPayload(
712
- def: { id: string; name: string; runner?: 'main' | 'isolated'; host_match?: string; login_redirect?: string },
712
+ def: { id: string; name: string; runner?: 'main' | 'isolated'; host_match?: string; login_redirect?: string; tab_strategy?: 'reuse' | 'ephemeral' },
713
713
  entry: ConnectorEntry,
714
714
  settings: Record<string, any>,
715
715
  ) {
@@ -747,6 +747,11 @@ function buildConnectorPayload(
747
747
  host_match: hostMatch,
748
748
  login_redirect: loginRedirect,
749
749
  runner: def.runner || entry.runner || 'main',
750
+ // Connector-level opt-in: 'ephemeral' = always open a fresh background
751
+ // tab, close after. Missing/'reuse' = pick an existing matching tab.
752
+ // Was being silently dropped — extension never saw it, so manifests
753
+ // with tab_strategy:ephemeral (mantis, tp) ran in reuse mode.
754
+ ...(def.tab_strategy ? { tab_strategy: def.tab_strategy } : {}),
750
755
  entries: [expandedEntry],
751
756
  };
752
757
  }
@@ -340,6 +340,57 @@ X-Forge-Token: …
340
340
  or `multipart/form-data` with a `file` field accepting `.yaml`,
341
341
  `.yml`, or `.zip`. Zip must have `manifest.yaml` at the root.
342
342
 
343
+ ### POST /api/connectors/import-config-template
344
+
345
+ Bulk-fill `connector-configs.json` from a JSON template. The Settings →
346
+ Connectors panel exposes this as the **↥ Import Template** button.
347
+
348
+ The template format:
349
+
350
+ ```json
351
+ {
352
+ "_README": "...",
353
+ "_prompts": {
354
+ "gitlab_pat": {
355
+ "label": "GitLab PAT",
356
+ "hint": "where to grab it",
357
+ "secret": true,
358
+ "required": true
359
+ }
360
+ },
361
+ "gitlab": {
362
+ "config": { "base_url": "https://...", "token": "${gitlab_pat}" },
363
+ "enabled": true
364
+ }
365
+ }
366
+ ```
367
+
368
+ Behavior:
369
+
370
+ - Two-phase: `multipart/form-data` (analyze) returns deduplicated list of
371
+ `${key}` placeholders the user still needs to fill. `application/json
372
+ {template, values}` (apply) substitutes and merges.
373
+ - Same placeholder key referenced from multiple connectors (e.g.
374
+ `gitlab_pat` used by `gitlab.token` AND `jenkins.instances[0].gitlab_pat`)
375
+ is asked **once** and applied to every target.
376
+ - Static values (non-placeholder) are applied as-is.
377
+ - **Existing non-empty fields are preserved** — the template never
378
+ overwrites a real token a user already configured (a `TODO_*` carry-over
379
+ is treated as empty and gets overwritten).
380
+ - Connectors whose manifest is not installed yet are skipped and reported
381
+ in the result. Sync the marketplace first, then re-import.
382
+ - Keys starting with `_` (`_README`, `_prompts`, etc.) are metadata.
383
+
384
+ Forge ships a default template at `templates/connector-config-template.json`,
385
+ bundled into the build. The Import button always works out of the box.
386
+ Users can override the bundled one by dropping their own at
387
+ `<dataDir>/config-template.json` — the GET endpoint picks the override
388
+ first, else falls back to the bundle.
389
+
390
+ Each prompt may include `url` + `url_label`, surfaced in the modal as
391
+ a "↗ Get token" link next to the field so the user can jump straight
392
+ to the page that issues the value.
393
+
343
394
  ## Migration from pre-v0.9
344
395
 
345
396
  Pre-v0.9 Forge stored connectors as built-in plugins under
package/lib/settings.ts CHANGED
@@ -64,6 +64,15 @@ export interface ApiProfile {
64
64
  model: string;
65
65
  apiKey: string;
66
66
  baseUrl?: string;
67
+ /**
68
+ * Total input-token ceiling for chat. Includes system prompt + connector
69
+ * catalog + memory context (summary + Temper recall) + history. Forge
70
+ * dynamically trims (memory tail dropped first, then oldest history)
71
+ * to keep the assembled prompt under this. Defaults to 200k (Claude
72
+ * Code's working window) when unset; lower it (e.g. 12000) for older
73
+ * 16k-context models that would otherwise truncate server-side.
74
+ */
75
+ maxInputTokens?: number;
67
76
  }
68
77
 
69
78
  /**
@@ -186,6 +195,12 @@ export interface Settings {
186
195
  pipelineTmpKeepFailedDays: number;
187
196
  pipelineTmpKeepCancelledDays: number;
188
197
  pipelineTmpGcIntervalHours: number;
198
+ /**
199
+ * First-run onboarding wizard. False (or absent) → Forge shows the
200
+ * wizard on every launch until the user completes it or skips. True
201
+ * → silent. Settings → "Re-run Onboarding" sets it back to false.
202
+ */
203
+ onboardingCompleted: boolean;
189
204
  }
190
205
 
191
206
  const defaults: Settings = {
@@ -236,6 +251,7 @@ const defaults: Settings = {
236
251
  pipelineTmpKeepFailedDays: 3,
237
252
  pipelineTmpKeepCancelledDays: 3,
238
253
  pipelineTmpGcIntervalHours: 6,
254
+ onboardingCompleted: false,
239
255
  };
240
256
 
241
257
  /** Decrypt nested apiKey fields in agents (legacy migration window) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.36",
3
+ "version": "0.10.38",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,131 @@
1
+ {
2
+ "_README": "Team template — import via Forge UI: Settings → Connectors → 'Import Template' button. Forge scans the ${key} placeholders, asks you once for each (shared keys like gitlab_pat are asked once and applied everywhere). After import, existing non-empty values are preserved — template never overwrites your real tokens.",
3
+
4
+ "_prompts": {
5
+ "gitlab_token_name": {
6
+ "label": "GitLab token name",
7
+ "hint": "Identifier of your GitLab token (typically your AD/LDAP login, e.g. 'zliu'). Reused by Jenkins as the gitlab_token_name + injected as TOKEN_USER build param.",
8
+ "group": "gitlab",
9
+ "required": true
10
+ },
11
+ "gitlab_pat": {
12
+ "label": "GitLab Personal Access Token",
13
+ "hint": "Scope: read_api + api. Reused as TOKEN_PASSWORD build param for Jenkins-triggered jobs.",
14
+ "url": "https://dops-git106.fortinet-us.com/-/user_settings/personal_access_tokens",
15
+ "url_label": "Open GitLab PAT page",
16
+ "secret": true,
17
+ "required": true
18
+ },
19
+ "jenkins_username": {
20
+ "label": "Jenkins username",
21
+ "hint": "Your Jenkins login (AD/LDAP), e.g. 'zliu'.",
22
+ "group": "jenkins",
23
+ "required": true
24
+ },
25
+ "jenkins_api_token": {
26
+ "label": "Jenkins API Token",
27
+ "hint": "User avatar → Configure → API Token → Add new",
28
+ "url": "http://nac-dev-jenkins.fortinet-us.com:8080/me/configure",
29
+ "url_label": "Open Jenkins user configure page",
30
+ "secret": true,
31
+ "required": true,
32
+ "group": "jenkins"
33
+ },
34
+ "blackduck_api_token": {
35
+ "label": "Black Duck API Token",
36
+ "hint": "User menu → My Access Tokens → Create new. Forge auto-exchanges for ~2h JWT.",
37
+ "url": "https://dops-blackduck.fortinet-us.com/api/current-user/tokens",
38
+ "url_label": "Open Black Duck tokens page",
39
+ "secret": true,
40
+ "required": true
41
+ },
42
+ "nac_admin_password": {
43
+ "label": "NAC admin password (optional)",
44
+ "hint": "Used by SSH + REST login() fallback. Leave blank if you'll pass per-call.",
45
+ "secret": true,
46
+ "required": false
47
+ },
48
+ "fortincm_password": {
49
+ "label": "FortiNCM admin password (optional)",
50
+ "hint": "Only needed if you use NCM",
51
+ "secret": true,
52
+ "required": false
53
+ }
54
+ },
55
+
56
+ "gitlab": {
57
+ "config": {
58
+ "base_url": "https://dops-git106.fortinet-us.com/",
59
+ "token": "${gitlab_pat}"
60
+ },
61
+ "enabled": true
62
+ },
63
+
64
+ "mantis": {
65
+ "config": {
66
+ "base_url": "https://mantis.fortinet.com/",
67
+ "default_project": "FortiNAC"
68
+ },
69
+ "enabled": true
70
+ },
71
+
72
+ "pmdb": {
73
+ "config": {
74
+ "base_url": "https://pmdb.fortinet.com/"
75
+ },
76
+ "enabled": true
77
+ },
78
+
79
+ "teams": {
80
+ "config": {
81
+ "base_url": "https://teams.microsoft.com/"
82
+ },
83
+ "enabled": true
84
+ },
85
+
86
+ "jenkins": {
87
+ "config": {
88
+ "instances": "[{\"name\":\"default-jenkins\",\"base_url\":\"http://nac-dev-jenkins.fortinet-us.com:8080\",\"username\":\"${jenkins_username}\",\"api_token\":\"${jenkins_api_token}\",\"gitlab_pat\":\"${gitlab_pat}\",\"gitlab_token_name\":\"${gitlab_token_name}\",\"gitlab_token_name_param\":\"${gitlab_token_name}\",\"gitlab_pat_param\":\"\",\"inject_params\":\"[{\\\"name\\\":\\\"TOKEN_USER\\\",\\\"value\\\":\\\"${gitlab_token_name}\\\"},{\\\"name\\\":\\\"TOKEN_PASSWORD\\\",\\\"value\\\":\\\"${gitlab_pat}\\\"}]\"}]"
89
+ },
90
+ "enabled": true
91
+ },
92
+
93
+ "tp": {
94
+ "config": {
95
+ "base_url": "https://nac-tp.fortinet-us.com",
96
+ "api_base_url": "https://nac-tp.fortinet-us.com:8000",
97
+ "username": "${user_name}"
98
+ },
99
+ "enabled": true
100
+ },
101
+
102
+ "nac": {
103
+ "config": {
104
+ "port": "22",
105
+ "username": "admin",
106
+ "password": "${nac_admin_password}"
107
+ },
108
+ "enabled": true
109
+ },
110
+
111
+ "fortincm": {
112
+ "config": {
113
+ "username": "admin",
114
+ "password": "${fortincm_password}"
115
+ },
116
+ "enabled": true
117
+ },
118
+
119
+ "scap": {
120
+ "config": {},
121
+ "enabled": true
122
+ },
123
+
124
+ "blackduck": {
125
+ "config": {
126
+ "base_url": "https://dops-blackduck.fortinet-us.com",
127
+ "api_token": "${blackduck_api_token}"
128
+ },
129
+ "enabled": true
130
+ }
131
+ }