@aion0/forge 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE_NOTES.md +6 -6
- package/app/api/connectors/[id]/settings/route.ts +31 -37
- package/app/api/connectors/[id]/test/route.ts +260 -0
- package/app/api/connectors/install-local/route.ts +211 -0
- package/app/api/connectors/marketplace/route.ts +79 -0
- package/app/api/connectors/route.ts +41 -46
- package/app/api/jobs/route.ts +4 -0
- package/app/api/skills/install-local/route.ts +282 -0
- package/components/ConnectorsPanel.tsx +526 -211
- package/components/SettingsModal.tsx +1 -0
- package/components/SkillsPanel.tsx +42 -1
- package/lib/agents/claude-adapter.ts +4 -0
- package/lib/agents/types.ts +6 -0
- package/lib/chat/agent-loop.ts +13 -22
- package/lib/chat/protocols/http.ts +1 -1
- package/lib/chat/protocols/shell.ts +1 -1
- package/lib/chat/tool-dispatcher.ts +20 -20
- package/lib/connectors/migration.ts +110 -0
- package/lib/connectors/registry.ts +328 -0
- package/lib/connectors/sync.ts +305 -0
- package/lib/connectors/types.ts +253 -0
- package/lib/help-docs/00-overview.md +1 -0
- package/lib/help-docs/17-connectors.md +241 -189
- package/lib/help-docs/21-build-connector.md +314 -0
- package/lib/help-docs/CLAUDE.md +4 -2
- package/lib/init.ts +25 -0
- package/lib/jobs/dispatcher.ts +28 -8
- package/lib/jobs/scheduler.ts +66 -6
- package/lib/jobs/store.ts +51 -2
- package/lib/jobs/types.ts +32 -0
- package/lib/pipeline-scheduler.ts +3 -2
- package/lib/pipeline.ts +137 -15
- package/lib/plugins/registry.ts +9 -42
- package/lib/plugins/types.ts +4 -129
- package/lib/settings.ts +7 -0
- package/lib/skills.ts +27 -1
- package/lib/task-manager.ts +62 -2
- package/package.json +4 -1
- package/src/core/db/database.ts +4 -0
- package/lib/builtin-plugins/github-api.yaml +0 -93
- package/lib/builtin-plugins/gitlab.yaml +0 -860
- package/lib/builtin-plugins/mantis.probe.js +0 -176
- package/lib/builtin-plugins/mantis.yaml +0 -964
- package/lib/builtin-plugins/pmdb.yaml +0 -178
- package/lib/builtin-plugins/teams.yaml +0 -913
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
|
|
4
4
|
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
5
5
|
|
|
6
6
|
const PluginsPanel = lazy(() => import('./PluginsPanel'));
|
|
@@ -25,6 +25,7 @@ interface Skill {
|
|
|
25
25
|
hasUpdate: boolean;
|
|
26
26
|
installedProjects: string[];
|
|
27
27
|
deletedRemotely: boolean;
|
|
28
|
+
source?: 'registry' | 'local';
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
interface ProjectInfo {
|
|
@@ -238,6 +239,26 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
238
239
|
};
|
|
239
240
|
|
|
240
241
|
const [syncProgress, setSyncProgress] = useState('');
|
|
242
|
+
const [uploading, setUploading] = useState(false);
|
|
243
|
+
const skillUploadRef = useRef<HTMLInputElement | null>(null);
|
|
244
|
+
|
|
245
|
+
async function uploadSkillFile(file: File) {
|
|
246
|
+
setUploading(true);
|
|
247
|
+
try {
|
|
248
|
+
const fd = new FormData();
|
|
249
|
+
fd.append('file', file);
|
|
250
|
+
const r = await fetch('/api/skills/install-local', { method: 'POST', body: fd });
|
|
251
|
+
const j = await r.json();
|
|
252
|
+
if (!r.ok || j.ok === false) {
|
|
253
|
+
alert(`Upload failed: ${j.error || `HTTP ${r.status}`}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
await fetchSkills();
|
|
257
|
+
} catch (e) {
|
|
258
|
+
alert(`Upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
259
|
+
} finally { setUploading(false); }
|
|
260
|
+
}
|
|
261
|
+
|
|
241
262
|
const sync = async () => {
|
|
242
263
|
setSyncing(true);
|
|
243
264
|
setSyncProgress('');
|
|
@@ -390,6 +411,26 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
390
411
|
</div>
|
|
391
412
|
</div>
|
|
392
413
|
<span className="text-[8px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-400">Claude Code</span>
|
|
414
|
+
<input
|
|
415
|
+
ref={skillUploadRef}
|
|
416
|
+
type="file"
|
|
417
|
+
accept=".md,.zip"
|
|
418
|
+
className="hidden"
|
|
419
|
+
onChange={(e) => {
|
|
420
|
+
const f = e.target.files?.[0];
|
|
421
|
+
e.target.value = '';
|
|
422
|
+
if (f) void uploadSkillFile(f);
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
<button
|
|
426
|
+
type="button"
|
|
427
|
+
onClick={() => skillUploadRef.current?.click()}
|
|
428
|
+
disabled={uploading}
|
|
429
|
+
title="Upload a local skill — SKILL.md or a .zip bundle. Falls outside the forge-skills registry."
|
|
430
|
+
className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50"
|
|
431
|
+
>
|
|
432
|
+
{uploading ? 'Uploading…' : '+ Upload'}
|
|
433
|
+
</button>
|
|
393
434
|
<button
|
|
394
435
|
onClick={sync}
|
|
395
436
|
disabled={syncing}
|
|
@@ -62,6 +62,10 @@ export function createClaudeAdapter(config: AgentConfig): AgentAdapter {
|
|
|
62
62
|
args.push('--resume', opts.conversationId);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
if (opts.appendSystemPrompt && opts.appendSystemPrompt.trim()) {
|
|
66
|
+
args.push('--append-system-prompt', opts.appendSystemPrompt);
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
if (opts.extraFlags) {
|
|
66
70
|
args.push(...opts.extraFlags);
|
|
67
71
|
}
|
package/lib/agents/types.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface AgentSpawnOptions {
|
|
|
42
42
|
skipPermissions?: boolean;
|
|
43
43
|
outputFormat?: 'stream-json' | 'json' | 'text';
|
|
44
44
|
extraFlags?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Extra text appended to the agent's system prompt for this run.
|
|
47
|
+
* Maps to `--append-system-prompt` on Claude Code. Adapters that
|
|
48
|
+
* don't support the concept ignore it.
|
|
49
|
+
*/
|
|
50
|
+
appendSystemPrompt?: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export interface AgentSpawnResult {
|
package/lib/chat/agent-loop.ts
CHANGED
|
@@ -27,11 +27,10 @@ import { renderMemoryContext } from './temper';
|
|
|
27
27
|
import { getMemoryStore } from './memory-store';
|
|
28
28
|
import { buildMemoryTools } from './memory-tools';
|
|
29
29
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from '../plugins/registry';
|
|
30
|
+
listInstalledConnectors,
|
|
31
|
+
getConnector,
|
|
32
|
+
getConnectorEntries,
|
|
33
|
+
} from '../connectors/registry';
|
|
35
34
|
import type {
|
|
36
35
|
ContentBlock,
|
|
37
36
|
Message,
|
|
@@ -224,18 +223,10 @@ function detectMentionedConnectors(
|
|
|
224
223
|
|
|
225
224
|
function buildConnectorTools(): LlmTool[] {
|
|
226
225
|
const out: LlmTool[] = [];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const def =
|
|
230
|
-
|
|
231
|
-
// We deliberately DO NOT require getInstalledPlugin(s.id) here — the LLM
|
|
232
|
-
// should always see all connector tools the user has defined. Install
|
|
233
|
-
// status only matters at dispatch time: browser tools that lack a
|
|
234
|
-
// configured base_url will surface a clear error inside the tool_result
|
|
235
|
-
// (better than vanishing from the tool list and having the LLM make up
|
|
236
|
-
// an excuse like "I can't see your Teams"). http/shell tools don't need
|
|
237
|
-
// install at all.
|
|
238
|
-
for (const entry of getConnectorsForPlugin(def)) {
|
|
226
|
+
for (const inst of listInstalledConnectors()) {
|
|
227
|
+
if (!inst.enabled) continue;
|
|
228
|
+
const def = inst.definition;
|
|
229
|
+
for (const entry of getConnectorEntries(def)) {
|
|
239
230
|
for (const [toolName, tool] of Object.entries(entry.tools || {})) {
|
|
240
231
|
// Executable if it has a script (browser protocol) OR a non-browser
|
|
241
232
|
// protocol that runs server-side (http / shell).
|
|
@@ -249,8 +240,8 @@ function buildConnectorTools(): LlmTool[] {
|
|
|
249
240
|
if ((pdef as any)?.required) required.push(pname);
|
|
250
241
|
}
|
|
251
242
|
out.push({
|
|
252
|
-
name: `${
|
|
253
|
-
description: (tool.description || `${
|
|
243
|
+
name: `${def.id}.${toolName}`,
|
|
244
|
+
description: (tool.description || `${def.name} · ${toolName}`).trim(),
|
|
254
245
|
input_schema: {
|
|
255
246
|
type: 'object',
|
|
256
247
|
properties,
|
|
@@ -361,9 +352,9 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
361
352
|
// restrict the tool list so the LLM can't wander to unrelated connectors.
|
|
362
353
|
// Strong (slash-prefix) signals also emit a directive in the system prompt
|
|
363
354
|
// so the model treats it as a command, not a hint.
|
|
364
|
-
const
|
|
365
|
-
const pluginCatalog =
|
|
366
|
-
const def =
|
|
355
|
+
const allConnectorIds = [...new Set(connectorTools.map((t) => t.name.split('.')[0]!))];
|
|
356
|
+
const pluginCatalog = allConnectorIds.map((id) => {
|
|
357
|
+
const def = getConnector(id);
|
|
367
358
|
return { id, name: def?.name };
|
|
368
359
|
});
|
|
369
360
|
const mentioned = detectMentionedConnectors(args.userText, pluginCatalog);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* is_error so the LLM can react.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { HttpRequestSpec, ConnectorTool } from '../../
|
|
18
|
+
import type { HttpRequestSpec, ConnectorTool } from '../../connectors/types';
|
|
19
19
|
import { expandAllTokens } from '../../plugins/templates';
|
|
20
20
|
|
|
21
21
|
export interface HttpProtocolArgs {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* automatic command allow-list in v1.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { ConnectorTool } from '../../
|
|
18
|
+
import type { ConnectorTool } from '../../connectors/types';
|
|
19
19
|
import { expandAllTokens } from '../../plugins/templates';
|
|
20
20
|
import { spawn } from 'node:child_process';
|
|
21
21
|
|
|
@@ -13,12 +13,12 @@ import { bridgeRpc } from './bridge-client';
|
|
|
13
13
|
import { runHttp } from './protocols/http';
|
|
14
14
|
import { runShell } from './protocols/shell';
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '../
|
|
16
|
+
getConnector,
|
|
17
|
+
getInstalledConnector,
|
|
18
|
+
getConnectorEntries,
|
|
19
|
+
} from '../connectors/registry';
|
|
20
20
|
import { expandSettingsTokens } from '../plugins/templates';
|
|
21
|
-
import type {
|
|
21
|
+
import type { ConnectorEntry, ConnectorTool } from '../connectors/types';
|
|
22
22
|
|
|
23
23
|
export interface ToolCall {
|
|
24
24
|
id: string;
|
|
@@ -55,26 +55,26 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
55
55
|
|
|
56
56
|
// ─── Connector dispatch ──────────────────────────────────
|
|
57
57
|
|
|
58
|
-
/** Look up a connector tool by `<
|
|
58
|
+
/** Look up a connector tool by `<connector_id>.<tool_name>` form. */
|
|
59
59
|
function findConnectorTool(qualified: string): {
|
|
60
|
-
|
|
60
|
+
connectorId: string;
|
|
61
61
|
toolName: string;
|
|
62
|
-
entry:
|
|
62
|
+
entry: ConnectorEntry;
|
|
63
63
|
tool: ConnectorTool;
|
|
64
64
|
settings: Record<string, any>;
|
|
65
65
|
} | null {
|
|
66
66
|
const dot = qualified.indexOf('.');
|
|
67
67
|
if (dot < 1) return null;
|
|
68
|
-
const
|
|
68
|
+
const connectorId = qualified.slice(0, dot);
|
|
69
69
|
const toolName = qualified.slice(dot + 1);
|
|
70
|
-
const def =
|
|
71
|
-
if (!def
|
|
72
|
-
const entries =
|
|
70
|
+
const def = getConnector(connectorId);
|
|
71
|
+
if (!def) return null;
|
|
72
|
+
const entries = getConnectorEntries(def);
|
|
73
73
|
for (const entry of entries) {
|
|
74
74
|
const tool = entry.tools?.[toolName];
|
|
75
75
|
if (tool) {
|
|
76
|
-
const inst =
|
|
77
|
-
return {
|
|
76
|
+
const inst = getInstalledConnector(connectorId);
|
|
77
|
+
return { connectorId, toolName, entry, tool, settings: (inst?.config as Record<string, any>) || {} };
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
@@ -86,8 +86,8 @@ function findConnectorTool(qualified: string): {
|
|
|
86
86
|
* the extension's runner finishes those at execution time.
|
|
87
87
|
*/
|
|
88
88
|
function buildConnectorPayload(
|
|
89
|
-
def: { id: string; name: string;
|
|
90
|
-
entry:
|
|
89
|
+
def: { id: string; name: string; runner?: 'main' | 'isolated'; host_match?: string; login_redirect?: string },
|
|
90
|
+
entry: ConnectorEntry,
|
|
91
91
|
settings: Record<string, any>,
|
|
92
92
|
) {
|
|
93
93
|
const expand = (s: string | undefined) => (s ? expandSettingsTokens(s, settings) : s);
|
|
@@ -117,9 +117,9 @@ function buildConnectorPayload(
|
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
return {
|
|
120
|
-
plugin_id: def.id,
|
|
120
|
+
plugin_id: def.id, // legacy wire-name; extension reads this
|
|
121
121
|
name: def.name,
|
|
122
|
-
mode:
|
|
122
|
+
mode: 'browser-side',
|
|
123
123
|
installed: true,
|
|
124
124
|
host_match: hostMatch,
|
|
125
125
|
login_redirect: loginRedirect,
|
|
@@ -157,7 +157,7 @@ export async function dispatchTool(
|
|
|
157
157
|
return { content: `unknown tool: ${call.name}`, is_error: true };
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
const def =
|
|
160
|
+
const def = getConnector(located.connectorId)!;
|
|
161
161
|
const protocol = located.tool.protocol || 'browser';
|
|
162
162
|
const argInput = (call.input ?? {}) as Record<string, any>;
|
|
163
163
|
|
|
@@ -173,7 +173,7 @@ export async function dispatchTool(
|
|
|
173
173
|
// the runner logic (tab acquire, navigate, executeScript).
|
|
174
174
|
const connector = buildConnectorPayload(def, located.entry, located.settings);
|
|
175
175
|
const result = (await bridgeRpc('connector.run', {
|
|
176
|
-
pluginId: located.
|
|
176
|
+
pluginId: located.connectorId, // wire-name kept for extension
|
|
177
177
|
toolName: located.toolName,
|
|
178
178
|
input: argInput,
|
|
179
179
|
connector,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot migration: lift connector rows out of plugin-configs.json
|
|
3
|
+
* into connector-configs.json.
|
|
4
|
+
*
|
|
5
|
+
* Pre-v0.9 Forge stored connector configs under plugin-configs.json
|
|
6
|
+
* with `category: connector` on the plugin def. From v0.9 connectors
|
|
7
|
+
* are an independent subsystem; configs live in connector-configs.json
|
|
8
|
+
* and manifests come from the remote forge-connectors registry (no
|
|
9
|
+
* built-in fallback).
|
|
10
|
+
*
|
|
11
|
+
* Migration moves the encrypted config blob (PAT, base_url, etc.)
|
|
12
|
+
* keyed by KNOWN_CONNECTOR_IDS — the set of connector ids that used
|
|
13
|
+
* to be built in. Manifests are NOT written here; they arrive on the
|
|
14
|
+
* next syncRegistry() call. If the user is offline at upgrade time,
|
|
15
|
+
* the marketplace will be empty until they reconnect — config rows
|
|
16
|
+
* are preserved so re-install restores everything.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent: once a connector id has been moved, its row is gone
|
|
19
|
+
* from plugin-configs.json and subsequent calls are no-ops.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { getDataDir } from '../dirs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* IDs that used to ship as built-in connectors in lib/builtin-plugins/.
|
|
28
|
+
* Add to this list when promoting a new built-in connector into the
|
|
29
|
+
* marketplace, but only for the upgrade window — once your user base
|
|
30
|
+
* has restarted with the new code, the row is gone and the entry
|
|
31
|
+
* becomes inert.
|
|
32
|
+
*/
|
|
33
|
+
const KNOWN_CONNECTOR_IDS = ['mantis', 'gitlab', 'teams', 'pmdb', 'github-api'];
|
|
34
|
+
|
|
35
|
+
function pluginConfigsPath(): string {
|
|
36
|
+
return join(getDataDir(), 'plugin-configs.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function connectorConfigsPath(): string {
|
|
40
|
+
return join(getDataDir(), 'connector-configs.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureDir(p: string): void {
|
|
44
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true, mode: 0o700 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ConnectorConfigRow {
|
|
48
|
+
config: Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Version recorded at migration time. We don't know what version
|
|
51
|
+
* the user was running before — write '0.0.0' so the next sync,
|
|
52
|
+
* which sees the registry version > 0.0.0, flags an update and
|
|
53
|
+
* pulls the manifest. The marketplace UI shows it as "Update
|
|
54
|
+
* available" briefly until that completes.
|
|
55
|
+
*/
|
|
56
|
+
installed_version: string;
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function migrateConnectorConfigs(): { migrated: number; skipped: number } {
|
|
61
|
+
const oldPath = pluginConfigsPath();
|
|
62
|
+
if (!existsSync(oldPath)) return { migrated: 0, skipped: 0 };
|
|
63
|
+
|
|
64
|
+
let oldStore: Record<string, any>;
|
|
65
|
+
try { oldStore = JSON.parse(readFileSync(oldPath, 'utf-8')); }
|
|
66
|
+
catch { return { migrated: 0, skipped: 0 }; }
|
|
67
|
+
|
|
68
|
+
const newPath = connectorConfigsPath();
|
|
69
|
+
let newStore: Record<string, ConnectorConfigRow> = {};
|
|
70
|
+
if (existsSync(newPath)) {
|
|
71
|
+
try { newStore = JSON.parse(readFileSync(newPath, 'utf-8')); }
|
|
72
|
+
catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let migrated = 0;
|
|
76
|
+
let skipped = 0;
|
|
77
|
+
const toRemoveFromOld: string[] = [];
|
|
78
|
+
|
|
79
|
+
for (const id of KNOWN_CONNECTOR_IDS) {
|
|
80
|
+
const oldRow = oldStore[id];
|
|
81
|
+
if (!oldRow) {
|
|
82
|
+
skipped += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!newStore[id]) {
|
|
86
|
+
newStore[id] = {
|
|
87
|
+
config: oldRow.config || {},
|
|
88
|
+
installed_version: '0.0.0',
|
|
89
|
+
enabled: oldRow.enabled !== false,
|
|
90
|
+
};
|
|
91
|
+
migrated += 1;
|
|
92
|
+
}
|
|
93
|
+
toRemoveFromOld.push(id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (toRemoveFromOld.length > 0) {
|
|
97
|
+
ensureDir(getDataDir());
|
|
98
|
+
writeFileSync(newPath, JSON.stringify(newStore, null, 2), { mode: 0o600 });
|
|
99
|
+
|
|
100
|
+
for (const id of toRemoveFromOld) delete oldStore[id];
|
|
101
|
+
writeFileSync(oldPath, JSON.stringify(oldStore, null, 2), { mode: 0o600 });
|
|
102
|
+
|
|
103
|
+
console.log(
|
|
104
|
+
`[connectors] migration: moved ${migrated} row(s) from plugin-configs.json. ` +
|
|
105
|
+
'Manifests will arrive on the next registry sync.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { migrated, skipped };
|
|
110
|
+
}
|