@aion0/forge 0.10.34 → 0.10.35

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,14 +1,12 @@
1
- # Forge v0.10.34
1
+ # Forge v0.10.35
2
2
 
3
3
  Released: 2026-06-03
4
4
 
5
- ## Changes since v0.10.33
5
+ ## Changes since v0.10.34
6
6
 
7
7
  ### Other
8
- - fix(watch): heuristic terminal detection for all connector pollers
9
- - ui(activity): segmented pill running/upcoming/failed each their own color
10
- - fix(watch): honor poll result's terminal: true regardless of done_match
11
- - fix(marketplace): scrollbar on long project list in install dropdown
8
+ - chat: namespace gating connector tools load on demand via connector_open
9
+ - DocTerminal: New/Resume buttons use configured agent, not hardcoded claude
12
10
 
13
11
 
14
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.33...v0.10.34
12
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.34...v0.10.35
@@ -166,13 +166,13 @@ export default function DocTerminal({ docRoot, agent }: { docRoot: string; agent
166
166
  </span>
167
167
  <div className="ml-auto flex items-center gap-1">
168
168
  <button
169
- onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude${sf}`); }}
169
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && ${agentCmdRef.current}${sf}`); }}
170
170
  className="text-[10px] px-2 py-0.5 text-[var(--accent)] hover:bg-[#2a2a4a] rounded"
171
171
  >
172
172
  New
173
173
  </button>
174
174
  <button
175
- onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude -c${sf}`); }}
175
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && ${agentCmdRef.current} -c${sf}`); }}
176
176
  className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[#2a2a4a] rounded"
177
177
  >
178
178
  Resume
@@ -207,7 +207,50 @@ export function resolveProvider(sessionProvider: string | null, sessionModel: st
207
207
  };
208
208
  }
209
209
 
210
- function buildSystemPrompt(connectorTools: LlmTool[], builtinDefs: typeof BUILTIN_TOOL_DEFS, sessionSystemPrompt: string | null): string {
210
+ /**
211
+ * Build the connector catalog block — one entry per installed connector,
212
+ * showing what it can do. The LLM reads this to decide whether to call
213
+ * connector_open(<id>) before reaching for a tool.
214
+ *
215
+ * Prefers `catalog_summary` from the manifest (curated 2-4 line English
216
+ * blurb). Falls back to first 5 tool names if absent, so older manifests
217
+ * still produce something usable.
218
+ */
219
+ function buildConnectorCatalog(openSet: Set<string>): string[] {
220
+ const lines: string[] = [];
221
+ for (const inst of listInstalledConnectors()) {
222
+ if (!inst.enabled) continue;
223
+ const def = inst.definition;
224
+ let toolCount = 0;
225
+ const sampleNames: string[] = [];
226
+ for (const entry of getConnectorEntries(def)) {
227
+ for (const tname of Object.keys(entry.tools || {})) {
228
+ toolCount += 1;
229
+ if (sampleNames.length < 5) sampleNames.push(tname);
230
+ }
231
+ }
232
+ const status = openSet.has(def.id) ? ' [OPEN]' : '';
233
+ const summary = (def.catalog_summary || '').trim();
234
+ if (summary) {
235
+ lines.push(`▸ ${def.id}${status} (${toolCount} tools):`);
236
+ for (const ln of summary.split('\n')) {
237
+ const trimmed = ln.trim();
238
+ if (trimmed) lines.push(` ${trimmed}`);
239
+ }
240
+ } else {
241
+ const sample = sampleNames.join(', ');
242
+ lines.push(`▸ ${def.id}${status}: ${toolCount} tools (e.g. ${sample}${toolCount > 5 ? ', …' : ''})`);
243
+ }
244
+ }
245
+ return lines;
246
+ }
247
+
248
+ function buildSystemPrompt(
249
+ openConnectorTools: LlmTool[],
250
+ openSet: Set<string>,
251
+ builtinDefs: typeof BUILTIN_TOOL_DEFS,
252
+ sessionSystemPrompt: string | null,
253
+ ): string {
211
254
  const now = new Date().toISOString();
212
255
 
213
256
  // Inject a brief Forge context block (project names only) so the LLM can
@@ -227,8 +270,10 @@ function buildSystemPrompt(connectorTools: LlmTool[], builtinDefs: typeof BUILTI
227
270
  `Current time: ${now}`,
228
271
  '',
229
272
  'Tool usage — IMPORTANT:',
230
- '- If the user mentions a system name (e.g. "teams", "mantis", "gitlab", "pmdb") even casually like "在 teams 中..." / "from mantis" you MUST attempt the matching connector tool FIRST.',
231
- ' Don\'t explain how to do something manually before trying the tool. The tools below run inside the user\'s actual logged-in browser session they CAN do things you might think only the user can do manually.',
273
+ '- Connector tools (mantis.*, gitlab.*, nac.*, tp.*, etc.) are NOT in your active tool list by default. The "Connector catalog" below shows what each connector can do.',
274
+ ' Call connector_open({name: "<id>"}) FIRST to load a connector its tools become callable on your next turn. The open set resets at every new user message, so re-open as the user pivots topics.',
275
+ '- If the user mentions a system name (e.g. "teams", "mantis", "gitlab", "pmdb") — even casually like "在 teams 中..." / "from mantis" — open that connector and use its tools.',
276
+ ' Don\'t explain how to do something manually before trying the tool. The connector tools run inside the user\'s actual logged-in browser session — they CAN do things you might think only the user can do manually.',
232
277
  '- For Teams in particular: send_message can target any chat by name; if the chat doesn\'t exist yet, the tool will return a specific error and THEN you can advise. Don\'t pre-judge.',
233
278
  '- If a tool call fails, read its error carefully — it usually tells you what to fix (wrong arg, missing setting, login required). Retry with the fix. Only give up after the tool explicitly says it cannot do the task.',
234
279
  '- For trigger_pipeline / dispatch_task: when the user names a "project" (e.g. "FortiNAC"), pass it as input.project verbatim. The names in the "Forge projects" list below ARE the valid values. Call list_forge_context only if you need paths / agents / skills.',
@@ -250,14 +295,16 @@ function buildSystemPrompt(connectorTools: LlmTool[], builtinDefs: typeof BUILTI
250
295
  lines.push('', `Forge projects (valid input.project values): ${projectNames.join(', ')}`);
251
296
  }
252
297
 
253
- if (connectorTools.length > 0) {
254
- lines.push('', 'Connector tools available:');
255
- for (const t of connectorTools) {
256
- lines.push(`- ${t.name}: ${t.description.slice(0, 100)}`);
298
+ const catalog = buildConnectorCatalog(openSet);
299
+ if (catalog.length > 0) {
300
+ lines.push('', 'Connector catalog — call connector_open({name}) to load tools:');
301
+ lines.push(...catalog);
302
+ if (openConnectorTools.length > 0) {
303
+ lines.push('', `Currently open connectors (${[...openSet].join(', ')}) — their tools ARE in your active tool list this turn.`);
257
304
  }
258
305
  }
259
306
  if (builtinDefs.length > 0) {
260
- lines.push('', 'Builtin tools:');
307
+ lines.push('', 'Builtin tools (always available):');
261
308
  for (const t of builtinDefs) {
262
309
  lines.push(`- ${t.name}: ${t.description.slice(0, 100)}`);
263
310
  }
@@ -460,36 +507,36 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
460
507
  });
461
508
  }
462
509
 
463
- let connectorTools = buildConnectorTools();
464
-
465
- // ── Narrowing: if the user named connectors (/teams, "in mantis", ...)
466
- // restrict the tool list so the LLM can't wander to unrelated connectors.
467
- // Strong (slash-prefix) signals also emit a directive in the system prompt
468
- // so the model treats it as a command, not a hint.
469
- const allConnectorIds = [...new Set(connectorTools.map((t) => t.name.split('.')[0]!))];
510
+ // ── Full connector tool inventory ────────────────────────────────
511
+ // We build the full set ONCE. The active `tools` field sent to the LLM
512
+ // is computed per iteration by filtering to the "open set" (connectors
513
+ // the LLM has opened via connector_open OR the user explicitly named).
514
+ const allConnectorTools = buildConnectorTools();
515
+ const allConnectorIds = [...new Set(allConnectorTools.map((t) => t.name.split('.')[0]!))];
470
516
  const pluginCatalog = allConnectorIds.map((id) => {
471
517
  const def = getConnector(id);
472
518
  return { id, name: def?.name };
473
519
  });
520
+
521
+ // User-mention auto-open: if the user's message names a connector (slash
522
+ // or bare), seed the open set so the LLM doesn't need an extra
523
+ // connector_open round-trip for the obvious cases. Strong signals
524
+ // (/connector) also emit a directive so the model treats it as a command.
474
525
  const mentioned = detectMentionedConnectors(args.userText, pluginCatalog);
475
- const narrowSet = mentioned.strong.size > 0 ? mentioned.strong
476
- : mentioned.medium.size > 0 ? mentioned.medium
477
- : null;
526
+ const autoOpenFromUserText: Set<string> = mentioned.strong.size > 0 ? mentioned.strong
527
+ : mentioned.medium.size > 0 ? mentioned.medium
528
+ : new Set();
478
529
  let narrowDirective = '';
479
- if (narrowSet) {
480
- connectorTools = connectorTools.filter((t) => narrowSet.has(t.name.split('.')[0]!));
481
- const list = [...narrowSet].join(', ');
482
- if (mentioned.strong.size > 0) {
483
- narrowDirective = `\n\nUSER MENTIONED CONNECTOR(S) EXPLICITLY (slash-prefix): ${list}. You MUST use these connector tools for this turn — do NOT answer without trying them first, and do NOT consider other connectors.`;
484
- console.log(`[chat] narrow STRONG → ${list}`);
485
- } else {
486
- console.log(`[chat] narrow MEDIUM → ${list}`);
487
- }
530
+ if (mentioned.strong.size > 0) {
531
+ const list = [...mentioned.strong].join(', ');
532
+ narrowDirective = `\n\nUSER MENTIONED CONNECTOR(S) EXPLICITLY (slash-prefix): ${list}. These are already open for this turn — use their tools directly, do NOT call connector_open for them.`;
533
+ console.log(`[chat] auto-open STRONG → ${list}`);
534
+ } else if (mentioned.medium.size > 0) {
535
+ console.log(`[chat] auto-open MEDIUM → ${[...mentioned.medium].join(', ')}`);
488
536
  }
489
537
 
490
538
  console.log(
491
- `[chat] tools=${connectorTools.length} ` +
492
- connectorTools.map((t) => t.name).join(', ').slice(0, 600),
539
+ `[chat] total connector tools=${allConnectorTools.length} across ${allConnectorIds.length} connectors`,
493
540
  );
494
541
 
495
542
  const builtinDefsAll = [
@@ -503,32 +550,64 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
503
550
  input_schema: t.input_schema,
504
551
  }));
505
552
 
506
- // ── Sticky narrow helper ─────────────────────────────────────────
507
- // After a turn that called connector tools, on the NEXT turn we
508
- // restrict tool list to ONLY the connectors that were used. This
509
- // shrinks tools from 99 ~10 in a typical mantis or nac flow,
510
- // saving ~18K tokens per turn AND letting the model focus its
511
- // attention (helps local models avoid hallucination).
512
- function pickConnectorNamespacesUsed(blocks: ContentBlock[]): Set<string> {
513
- const ns = new Set<string>();
514
- for (const b of blocks) {
515
- if (b.type === 'tool_use' && b.name.includes('.')) {
553
+ // ── Open set computation ─────────────────────────────────────────
554
+ // A connector is "open" for this user-task if the LLM has called
555
+ // connector_open(name=<id>) OR has called <id>.<tool> in any assistant
556
+ // turn since the most recent user TEXT message. User text messages mark
557
+ // task boundaries on the next user message, the open set resets to
558
+ // just what user-text auto-opens seed.
559
+ //
560
+ // assistantBlocksAccum captures the in-progress turn's blocks (not yet
561
+ // in history during the iteration). We OR them with the history scan.
562
+ function computeOpenSet(history: Message[], currentBlocks: ContentBlock[]): Set<string> {
563
+ const ns = new Set<string>(autoOpenFromUserText);
564
+ // Find the most recent user message that has text content. Tool-result
565
+ // user messages don't count as task boundaries.
566
+ let lastUserIdx = -1;
567
+ for (let i = history.length - 1; i >= 0; i--) {
568
+ const m = history[i]!;
569
+ if (m.role !== 'user') continue;
570
+ if (m.blocks.some((b) => b.type === 'text')) { lastUserIdx = i; break; }
571
+ }
572
+ const start = lastUserIdx + 1;
573
+ for (let i = start; i < history.length; i++) {
574
+ const m = history[i]!;
575
+ if (m.role !== 'assistant') continue;
576
+ for (const b of m.blocks) {
577
+ if (b.type !== 'tool_use') continue;
578
+ if (b.name === 'connector_open') {
579
+ const opened = (b.input as { name?: string } | undefined)?.name;
580
+ if (opened) ns.add(String(opened));
581
+ } else if (b.name.includes('.')) {
582
+ ns.add(b.name.split('.')[0]!);
583
+ }
584
+ }
585
+ }
586
+ for (const b of currentBlocks) {
587
+ if (b.type !== 'tool_use') continue;
588
+ if (b.name === 'connector_open') {
589
+ const opened = (b.input as { name?: string } | undefined)?.name;
590
+ if (opened) ns.add(String(opened));
591
+ } else if (b.name.includes('.')) {
516
592
  ns.add(b.name.split('.')[0]!);
517
593
  }
518
594
  }
519
595
  return ns;
520
596
  }
597
+
521
598
  const sessionSystemPrompt = session.system_prompt;
522
- function buildSystem(tools: LlmTool[]): string {
523
- let s = buildSystemPrompt(tools, builtinDefsAll, sessionSystemPrompt);
599
+ function buildSystem(openTools: LlmTool[], openSet: Set<string>): string {
600
+ let s = buildSystemPrompt(openTools, openSet, builtinDefsAll, sessionSystemPrompt);
524
601
  if (narrowDirective) s += narrowDirective;
525
602
  return s;
526
603
  }
527
604
 
528
- const baseConnectorTools = connectorTools; // post-initial-narrow snapshot
529
- let allTools: LlmTool[] = [...builtinToolDefs, ...baseConnectorTools];
605
+ // Initial open set (before any iteration): just user-text auto-open seeds
606
+ let openSet: Set<string> = new Set(autoOpenFromUserText);
607
+ let openConnectorTools = allConnectorTools.filter((t) => openSet.has(t.name.split('.')[0]!));
608
+ let allTools: LlmTool[] = [...builtinToolDefs, ...openConnectorTools];
530
609
 
531
- let system = buildSystem(baseConnectorTools);
610
+ let system = buildSystem(openConnectorTools, openSet);
532
611
  if (memContext) system += '\n\n─── Memory context (auto-loaded) ───\n' + memContext;
533
612
  if (memStore.enabled) {
534
613
  const searchHint = memStore.kind === 'local'
@@ -559,23 +638,21 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
559
638
  return { ok: false, error: 'empty history' };
560
639
  }
561
640
 
562
- // ── Sticky narrow: shrink tools to only what last turn actually used.
563
- // First iteration: keep the user-mention-narrowed list. Iter 2+:
564
- // if previous assistant turn called e.g. mantis.get_bug, restrict
565
- // to mantis.* only local models behave much better with focused
566
- // tool set, and we save ~18K tokens per turn.
567
- if (iter > 1 && assistantBlocksAccum.length > 0) {
568
- const usedNs = pickConnectorNamespacesUsed(assistantBlocksAccum);
569
- if (usedNs.size > 0) {
570
- const narrowedConn = baseConnectorTools.filter((t) =>
571
- usedNs.has(t.name.split('.')[0]!));
572
- if (narrowedConn.length > 0 && narrowedConn.length < baseConnectorTools.length) {
573
- allTools = [...builtinToolDefs, ...narrowedConn];
574
- system = buildSystem(narrowedConn);
575
- if (memContext) system += '\n\n─── Memory context (auto-loaded) ───\n' + memContext;
576
- console.log(`[chat] sticky narrow → ${[...usedNs].join(',')} (${narrowedConn.length}/${baseConnectorTools.length} connector tools)`);
577
- }
578
- }
641
+ // ── Recompute open set every iteration ──────────────────────
642
+ // Scan history (since last user text msg) + this turn's accumulated
643
+ // blocks which connectors are open right now. Then filter tools.
644
+ // First iteration: only user-text auto-opens seed the set. After
645
+ // the LLM calls connector_open, subsequent iterations pick that up.
646
+ const newOpenSet = computeOpenSet(history, assistantBlocksAccum);
647
+ const setChanged = newOpenSet.size !== openSet.size ||
648
+ [...newOpenSet].some((n) => !openSet.has(n));
649
+ if (setChanged) {
650
+ openSet = newOpenSet;
651
+ openConnectorTools = allConnectorTools.filter((t) => openSet.has(t.name.split('.')[0]!));
652
+ allTools = [...builtinToolDefs, ...openConnectorTools];
653
+ system = buildSystem(openConnectorTools, openSet);
654
+ if (memContext) system += '\n\n─── Memory context (auto-loaded) ───\n' + memContext;
655
+ console.log(`[chat] open set{${[...openSet].join(',')}} (${openConnectorTools.length} connector tools active)`);
579
656
  }
580
657
 
581
658
  assistantBlocksAccum = [];
@@ -336,6 +336,31 @@ const BUILTINS: Record<string, BuiltinHandler> = {
336
336
  ? content.slice(0, MAX) + `\n\n…[truncated — doc is ${content.length} chars]`
337
337
  : content;
338
338
  },
339
+
340
+ // Namespace gating meta-tool. Connector tools (mantis.*, gitlab.*, etc.)
341
+ // are NOT in the active tools list by default — only their catalog entry
342
+ // is visible in the system prompt. Calling connector_open({name}) makes
343
+ // that connector's tools available for the rest of this user-task (until
344
+ // the next user message). The handler returns a tiny confirmation; the
345
+ // actual side-effect is that agent-loop scans assistant history for
346
+ // these calls (and direct namespaced calls) to compute the "open set"
347
+ // each turn.
348
+ connector_open: async (input) => {
349
+ const { name } = (input as { name?: string } | undefined) || {};
350
+ if (!name) return JSON.stringify({ ok: false, error: 'name is required (e.g. "mantis", "gitlab", "nac")' });
351
+ const def = getConnector(String(name));
352
+ if (!def) return JSON.stringify({ ok: false, error: `unknown connector: ${name}. Call connector_open with a name from the catalog in the system prompt.` });
353
+ let toolCount = 0;
354
+ for (const entry of getConnectorEntries(def)) {
355
+ toolCount += Object.keys(entry.tools || {}).length;
356
+ }
357
+ return JSON.stringify({
358
+ ok: true,
359
+ connector: name,
360
+ tool_count: toolCount,
361
+ message: `${name} loaded — ${toolCount} tools available next turn. Call them as ${name}.<tool_name>.`,
362
+ });
363
+ },
339
364
  };
340
365
 
341
366
  export interface BuiltinToolDef {
@@ -446,6 +471,20 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
446
471
  required: ['doc'],
447
472
  },
448
473
  },
474
+ {
475
+ name: 'connector_open',
476
+ description: 'Load a connector to make its tools (e.g. mantis.search_bugs, gitlab.list_my_todos) available for use. REQUIRED before calling any connector tool — the catalog block in the system prompt shows what each connector can do. Tools stay loaded only for the current user task; the next user message resets the open set, so re-open as needed.',
477
+ input_schema: {
478
+ type: 'object',
479
+ properties: {
480
+ name: {
481
+ type: 'string',
482
+ description: 'Connector id from the catalog (e.g. "mantis", "gitlab", "nac", "tp", "jenkins", "teams", "pmdb", "github-api").',
483
+ },
484
+ },
485
+ required: ['name'],
486
+ },
487
+ },
449
488
  ];
450
489
 
451
490
  // ─── Connector dispatch ──────────────────────────────────
@@ -477,6 +477,14 @@ export interface ConnectorDefinition {
477
477
  author?: string;
478
478
  description?: string;
479
479
 
480
+ /**
481
+ * 2-4 line English summary of what this connector can do, shown in the
482
+ * chat system prompt's "Connector catalog" block. Drives the LLM's
483
+ * decision to call connector_open(<id>). When absent, the agent falls
484
+ * back to listing the first few tool names — informative but flat.
485
+ */
486
+ catalog_summary?: string;
487
+
480
488
  /**
481
489
  * Minimum Forge version this manifest expects. The registry filter
482
490
  * hides newer-than-supported manifests so users on older Forge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.34",
3
+ "version": "0.10.35",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {