@aion0/forge 0.8.0 → 0.8.2
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 +25 -101
- 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 +1 -0
- package/app/api/monitor/route.ts +26 -0
- package/app/api/skills/install-local/route.ts +282 -0
- package/components/ConnectorsPanel.tsx +526 -211
- package/components/MonitorPanel.tsx +43 -0
- 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 +14 -23
- package/lib/chat/local-memory.ts +2 -2
- package/lib/chat/memory-store.ts +1 -1
- package/lib/chat/protocols/http.ts +2 -2
- package/lib/chat/protocols/shell.ts +2 -2
- package/lib/chat/session-store.ts +2 -2
- package/lib/chat/tool-dispatcher.ts +21 -21
- 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 +21 -3
- package/lib/jobs/store.ts +11 -2
- package/lib/jobs/types.ts +12 -0
- package/lib/pipeline-scheduler.ts +3 -2
- package/lib/pipeline.ts +135 -13
- 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 +3 -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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/connectors/install-local
|
|
3
|
+
*
|
|
4
|
+
* Install a connector from a user-supplied manifest. Two flavours:
|
|
5
|
+
*
|
|
6
|
+
* multipart/form-data with field `file`:
|
|
7
|
+
* - .yaml / .yml → single-file manifest
|
|
8
|
+
* - .zip → multi-file bundle (must contain manifest.yaml
|
|
9
|
+
* at the root; may add README.md, icon.svg,
|
|
10
|
+
* tools/*.js, etc.)
|
|
11
|
+
*
|
|
12
|
+
* application/json body `{ yaml: "...", id?: "..." }`:
|
|
13
|
+
* - convenience path for callers that already have the YAML in
|
|
14
|
+
* memory (Forge Help AI, internal scripts). `id` is optional
|
|
15
|
+
* — if missing, parsed from the manifest.
|
|
16
|
+
*
|
|
17
|
+
* Behaviour:
|
|
18
|
+
* 1. Validate the manifest has `id`, `name`, and at least `tools` or
|
|
19
|
+
* `connectors[]`.
|
|
20
|
+
* 2. Write the manifest (and any zip siblings) to
|
|
21
|
+
* <dataDir>/connectors/<id>/.
|
|
22
|
+
* 3. Register/upsert the row in connector-configs.json with the
|
|
23
|
+
* manifest's version as installed_version, preserving any prior
|
|
24
|
+
* user-supplied settings (so re-installing a tweaked version
|
|
25
|
+
* doesn't blow away a PAT).
|
|
26
|
+
*
|
|
27
|
+
* Marketplace UI distinguishes "local" connectors from "registry"
|
|
28
|
+
* connectors by membership in the registry cache — no extra source
|
|
29
|
+
* field on disk.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { NextResponse } from 'next/server';
|
|
33
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
34
|
+
import { join, normalize, sep } from 'node:path';
|
|
35
|
+
import AdmZip from 'adm-zip';
|
|
36
|
+
import YAML from 'yaml';
|
|
37
|
+
import { getDataDir } from '@/lib/dirs';
|
|
38
|
+
import { installConnector } from '@/lib/connectors/registry';
|
|
39
|
+
|
|
40
|
+
const MAX_ZIP_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
41
|
+
const MAX_FILE_BYTES = 1 * 1024 * 1024; // 1 MB per file inside the zip
|
|
42
|
+
const MAX_FILES_IN_ZIP = 50;
|
|
43
|
+
|
|
44
|
+
interface ParsedManifest {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
version?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseManifest(yaml: string): ParsedManifest {
|
|
51
|
+
let def: any;
|
|
52
|
+
try {
|
|
53
|
+
def = YAML.parse(yaml);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new Error(`invalid YAML: ${(e as Error).message}`);
|
|
56
|
+
}
|
|
57
|
+
if (!def?.id || typeof def.id !== 'string') {
|
|
58
|
+
throw new Error('manifest.id is required');
|
|
59
|
+
}
|
|
60
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(def.id)) {
|
|
61
|
+
throw new Error(`manifest.id "${def.id}" must be lowercase alphanumerics + hyphens/underscores`);
|
|
62
|
+
}
|
|
63
|
+
if (!def.name || typeof def.name !== 'string') {
|
|
64
|
+
throw new Error('manifest.name is required');
|
|
65
|
+
}
|
|
66
|
+
if (!def.tools && !(Array.isArray(def.connectors) && def.connectors.length)) {
|
|
67
|
+
throw new Error('manifest must declare `tools` or `connectors[]`');
|
|
68
|
+
}
|
|
69
|
+
return { id: def.id, name: def.name, version: def.version };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function safeEntryPath(name: string): string | null {
|
|
73
|
+
// Reject absolute paths, parent-dir escapes, leading slashes.
|
|
74
|
+
if (!name || name.startsWith('/') || name.includes('..')) return null;
|
|
75
|
+
const normalized = normalize(name).replace(/^[/\\]+/, '');
|
|
76
|
+
if (normalized.split(sep).some((seg) => seg === '..' || seg === '')) return null;
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ensureDir(p: string): void {
|
|
81
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true, mode: 0o700 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface InstallResult {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
id?: string;
|
|
87
|
+
version?: string;
|
|
88
|
+
files_written?: number;
|
|
89
|
+
error?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function installFromFiles(
|
|
93
|
+
manifestYaml: string,
|
|
94
|
+
extraFiles: Array<{ path: string; data: Buffer }>,
|
|
95
|
+
): InstallResult {
|
|
96
|
+
let parsed: ParsedManifest;
|
|
97
|
+
try { parsed = parseManifest(manifestYaml); }
|
|
98
|
+
catch (e) { return { ok: false, error: (e as Error).message }; }
|
|
99
|
+
|
|
100
|
+
const dir = join(getDataDir(), 'connectors', parsed.id);
|
|
101
|
+
ensureDir(dir);
|
|
102
|
+
|
|
103
|
+
// Wipe stale siblings from a prior install of the same id (but
|
|
104
|
+
// preserve nothing — local install replaces the directory).
|
|
105
|
+
// For safety, we only write inside `dir`; safeEntryPath gated it.
|
|
106
|
+
|
|
107
|
+
writeFileSync(join(dir, 'manifest.yaml'), manifestYaml, { mode: 0o600 });
|
|
108
|
+
let written = 1;
|
|
109
|
+
for (const f of extraFiles) {
|
|
110
|
+
const target = join(dir, f.path);
|
|
111
|
+
ensureDir(join(target, '..'));
|
|
112
|
+
writeFileSync(target, f.data, { mode: 0o600 });
|
|
113
|
+
written += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ok = installConnector(parsed.id, manifestYaml);
|
|
117
|
+
if (!ok) {
|
|
118
|
+
return { ok: false, error: 'installConnector rejected the manifest (re-parse failed)' };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
id: parsed.id,
|
|
123
|
+
version: parsed.version || '0.0.0',
|
|
124
|
+
files_written: written,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function POST(req: Request) {
|
|
129
|
+
const ct = req.headers.get('content-type') || '';
|
|
130
|
+
|
|
131
|
+
// ── JSON path: { yaml: "...", id?: "..." } ─────────────────
|
|
132
|
+
if (ct.includes('application/json')) {
|
|
133
|
+
let body: any = {};
|
|
134
|
+
try { body = await req.json(); } catch {}
|
|
135
|
+
const yaml = typeof body?.yaml === 'string' ? body.yaml : '';
|
|
136
|
+
if (!yaml) {
|
|
137
|
+
return NextResponse.json({ ok: false, error: 'yaml field is required' }, { status: 400 });
|
|
138
|
+
}
|
|
139
|
+
const r = installFromFiles(yaml, []);
|
|
140
|
+
return NextResponse.json(r, { status: r.ok ? 200 : 400 });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── multipart: file = .yaml | .yml | .zip ──────────────────
|
|
144
|
+
if (ct.includes('multipart/form-data')) {
|
|
145
|
+
let form: FormData;
|
|
146
|
+
try { form = await req.formData(); }
|
|
147
|
+
catch (e) {
|
|
148
|
+
return NextResponse.json({ ok: false, error: 'invalid multipart body' }, { status: 400 });
|
|
149
|
+
}
|
|
150
|
+
const file = form.get('file');
|
|
151
|
+
if (!(file instanceof File)) {
|
|
152
|
+
return NextResponse.json({ ok: false, error: 'file field missing' }, { status: 400 });
|
|
153
|
+
}
|
|
154
|
+
if (file.size > MAX_ZIP_BYTES) {
|
|
155
|
+
return NextResponse.json({ ok: false, error: `file too large (max ${MAX_ZIP_BYTES} bytes)` }, { status: 413 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const name = file.name.toLowerCase();
|
|
159
|
+
const buf = Buffer.from(await file.arrayBuffer());
|
|
160
|
+
|
|
161
|
+
if (name.endsWith('.yaml') || name.endsWith('.yml')) {
|
|
162
|
+
const r = installFromFiles(buf.toString('utf-8'), []);
|
|
163
|
+
return NextResponse.json(r, { status: r.ok ? 200 : 400 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (name.endsWith('.zip')) {
|
|
167
|
+
let zip: AdmZip;
|
|
168
|
+
try { zip = new AdmZip(buf); }
|
|
169
|
+
catch (e) {
|
|
170
|
+
return NextResponse.json({ ok: false, error: `invalid zip: ${(e as Error).message}` }, { status: 400 });
|
|
171
|
+
}
|
|
172
|
+
const entries = zip.getEntries().filter((e) => !e.isDirectory);
|
|
173
|
+
if (entries.length === 0) {
|
|
174
|
+
return NextResponse.json({ ok: false, error: 'zip is empty' }, { status: 400 });
|
|
175
|
+
}
|
|
176
|
+
if (entries.length > MAX_FILES_IN_ZIP) {
|
|
177
|
+
return NextResponse.json({ ok: false, error: `zip has too many files (max ${MAX_FILES_IN_ZIP})` }, { status: 400 });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Locate manifest.yaml at the root.
|
|
181
|
+
const manifestEntry = entries.find((e) => {
|
|
182
|
+
const safe = safeEntryPath(e.entryName);
|
|
183
|
+
return safe === 'manifest.yaml' || safe === 'manifest.yml';
|
|
184
|
+
});
|
|
185
|
+
if (!manifestEntry) {
|
|
186
|
+
return NextResponse.json({ ok: false, error: 'zip must contain manifest.yaml at the root' }, { status: 400 });
|
|
187
|
+
}
|
|
188
|
+
const manifestYaml = manifestEntry.getData().toString('utf-8');
|
|
189
|
+
|
|
190
|
+
const extras: Array<{ path: string; data: Buffer }> = [];
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
if (e === manifestEntry) continue;
|
|
193
|
+
const safe = safeEntryPath(e.entryName);
|
|
194
|
+
if (!safe) {
|
|
195
|
+
return NextResponse.json({ ok: false, error: `unsafe path in zip: ${e.entryName}` }, { status: 400 });
|
|
196
|
+
}
|
|
197
|
+
const data = e.getData();
|
|
198
|
+
if (data.length > MAX_FILE_BYTES) {
|
|
199
|
+
return NextResponse.json({ ok: false, error: `entry too large: ${safe}` }, { status: 413 });
|
|
200
|
+
}
|
|
201
|
+
extras.push({ path: safe, data });
|
|
202
|
+
}
|
|
203
|
+
const r = installFromFiles(manifestYaml, extras);
|
|
204
|
+
return NextResponse.json(r, { status: r.ok ? 200 : 400 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return NextResponse.json({ ok: false, error: 'unsupported file extension (expected .yaml/.yml/.zip)' }, { status: 400 });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return NextResponse.json({ ok: false, error: 'expected JSON or multipart/form-data' }, { status: 400 });
|
|
211
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector Marketplace API
|
|
3
|
+
*
|
|
4
|
+
* GET /api/connectors/marketplace
|
|
5
|
+
* → { fetched_at, base_url, entries: ConnectorMarketEntry[] }
|
|
6
|
+
* Pure read — last cached registry merged with installed state.
|
|
7
|
+
* Returns an empty list when sync has never succeeded.
|
|
8
|
+
*
|
|
9
|
+
* POST /api/connectors/marketplace
|
|
10
|
+
* body: { action: 'sync' | 'install' | 'uninstall' | 'update', id?: string }
|
|
11
|
+
*
|
|
12
|
+
* - sync refresh registry.json (and installed manifests)
|
|
13
|
+
* - install pull manifest + write to <dataDir>/connectors/<id>/
|
|
14
|
+
* - uninstall drop manifest (config retained by default)
|
|
15
|
+
* - update alias for install when a newer version exists
|
|
16
|
+
*
|
|
17
|
+
* Auth handled by middleware (cookie or X-Forge-Token). All operations
|
|
18
|
+
* are local — secrets never leave the host.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { NextResponse } from 'next/server';
|
|
22
|
+
import {
|
|
23
|
+
listMarketplace,
|
|
24
|
+
syncRegistry,
|
|
25
|
+
installFromRegistry,
|
|
26
|
+
} from '@/lib/connectors/sync';
|
|
27
|
+
import {
|
|
28
|
+
uninstallConnector,
|
|
29
|
+
getInstalledConnector,
|
|
30
|
+
} from '@/lib/connectors/registry';
|
|
31
|
+
|
|
32
|
+
export async function GET() {
|
|
33
|
+
return NextResponse.json(listMarketplace());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function POST(req: Request) {
|
|
37
|
+
let body: any = {};
|
|
38
|
+
try { body = await req.json(); } catch {}
|
|
39
|
+
const action = String(body?.action || '').trim();
|
|
40
|
+
const id = body?.id ? String(body.id).trim() : '';
|
|
41
|
+
|
|
42
|
+
switch (action) {
|
|
43
|
+
case 'sync': {
|
|
44
|
+
const r = await syncRegistry({ refreshInstalled: !!body?.refreshInstalled });
|
|
45
|
+
return NextResponse.json(r);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'install':
|
|
49
|
+
case 'update': {
|
|
50
|
+
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
51
|
+
const r = await installFromRegistry(id);
|
|
52
|
+
if (!r.ok) return NextResponse.json(r, { status: 502 });
|
|
53
|
+
return NextResponse.json(r);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'uninstall': {
|
|
57
|
+
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
58
|
+
const keepConfig = body?.keepConfig !== false; // default true
|
|
59
|
+
const ok = uninstallConnector(id, keepConfig);
|
|
60
|
+
if (!ok) return NextResponse.json({ ok: false, error: 'uninstall failed' }, { status: 500 });
|
|
61
|
+
return NextResponse.json({ ok: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case 'status': {
|
|
65
|
+
// Quick "is this id installed and at what version?" probe — used
|
|
66
|
+
// by the UI to refresh a single row without reloading the list.
|
|
67
|
+
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
68
|
+
const inst = getInstalledConnector(id);
|
|
69
|
+
return NextResponse.json({
|
|
70
|
+
installed: !!inst,
|
|
71
|
+
installed_version: inst?.installed_version,
|
|
72
|
+
enabled: inst?.enabled ?? false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
return NextResponse.json({ error: `unknown action: ${action}` }, { status: 400 });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connectors API — discovery surface for the Forge browser extension.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Reads from the standalone connector registry (lib/connectors/), no
|
|
5
|
+
* longer from the plugin registry. The extension hits this endpoint to
|
|
6
|
+
* learn what connectors are installed and what tools each exposes;
|
|
7
|
+
* `page` / `host_match` / `login_redirect` are expanded server-side
|
|
8
|
+
* with the user's saved settings, {args.*} stays literal for the
|
|
9
|
+
* extension runner.
|
|
10
|
+
*
|
|
11
|
+
* Response shape is preserved 1:1 from the old plugin-backed
|
|
12
|
+
* implementation so the extension and Forge chat keep working without
|
|
13
|
+
* any client change.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import { NextResponse } from 'next/server';
|
|
14
17
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '@/lib/plugins/registry';
|
|
18
|
+
listInstalledConnectors,
|
|
19
|
+
getInstalledConnector,
|
|
20
|
+
getConnectorEntries,
|
|
21
|
+
} from '@/lib/connectors/registry';
|
|
20
22
|
import { expandSettingsTokens } from '@/lib/plugins/templates';
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
import type {
|
|
24
|
+
ConnectorDefinition,
|
|
25
|
+
ConnectorEntry,
|
|
26
|
+
ConnectorTool,
|
|
27
|
+
} from '@/lib/connectors/types';
|
|
24
28
|
|
|
25
29
|
function expandTool(tool: ConnectorTool, settings: Record<string, any> | undefined): ConnectorTool {
|
|
26
30
|
if (!tool.page && !tool.script) return tool;
|
|
@@ -35,8 +39,8 @@ function expandTool(tool: ConnectorTool, settings: Record<string, any> | undefin
|
|
|
35
39
|
return { ...tool, ...(page ? { page } : {}) };
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function
|
|
39
|
-
const out:
|
|
42
|
+
function expandEntry(entry: ConnectorEntry, settings: Record<string, any> | undefined): ConnectorEntry {
|
|
43
|
+
const out: ConnectorEntry = {
|
|
40
44
|
...entry,
|
|
41
45
|
tools: Object.fromEntries(
|
|
42
46
|
Object.entries(entry.tools).map(([name, t]) => [name, expandTool(t, settings)]),
|
|
@@ -47,36 +51,33 @@ function expandConnector(entry: Connector, settings: Record<string, any> | undef
|
|
|
47
51
|
return out;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function toConnectorPayload(def:
|
|
51
|
-
const
|
|
52
|
-
const settings = inst?.config;
|
|
53
|
-
const entries = getConnectorsForPlugin(def).map(e => expandConnector(e, settings));
|
|
54
|
+
function toConnectorPayload(def: ConnectorDefinition, config: Record<string, any> | undefined, installed: boolean) {
|
|
55
|
+
const entries = getConnectorEntries(def).map(e => expandEntry(e, config));
|
|
54
56
|
|
|
55
|
-
// Hoist host_match / login_redirect to plugin level for 1:1 cases — the
|
|
56
|
-
// common case (and what the spec example shows). Pull from the first entry
|
|
57
|
-
// when those came from the top-level plugin fields.
|
|
58
57
|
const hostMatch = def.host_match
|
|
59
|
-
? expandSettingsTokens(def.host_match,
|
|
58
|
+
? expandSettingsTokens(def.host_match, config)
|
|
60
59
|
: entries[0]?.host_match;
|
|
61
60
|
const loginRedirect = def.login_redirect
|
|
62
|
-
? expandSettingsTokens(def.login_redirect,
|
|
61
|
+
? expandSettingsTokens(def.login_redirect, config)
|
|
63
62
|
: entries[0]?.login_redirect;
|
|
64
|
-
// Default to 'main' so existing connectors (Mantis, GitLab) keep their
|
|
65
|
-
// current execution path. Plugins on strict-CSP sites opt into 'isolated'.
|
|
66
63
|
const runner = def.runner || entries[0]?.runner || 'main';
|
|
67
64
|
|
|
68
65
|
return {
|
|
69
|
-
plugin_id: def.id,
|
|
66
|
+
plugin_id: def.id, // legacy field name preserved
|
|
70
67
|
name: def.name,
|
|
71
68
|
icon: def.icon,
|
|
72
69
|
version: def.version,
|
|
73
70
|
author: def.author || 'forge',
|
|
74
71
|
description: def.description || '',
|
|
75
|
-
mode:
|
|
76
|
-
installed
|
|
72
|
+
mode: 'browser-side', // every connector is browser-facing
|
|
73
|
+
installed,
|
|
77
74
|
...(hostMatch ? { host_match: hostMatch } : {}),
|
|
78
75
|
...(loginRedirect ? { login_redirect: loginRedirect } : {}),
|
|
79
76
|
runner,
|
|
77
|
+
/** Hint for the marketplace UI: a Test button is wired for this connector. */
|
|
78
|
+
has_test: !!def.test,
|
|
79
|
+
/** Optional human description of what the test does (no request shape exposed). */
|
|
80
|
+
test_description: def.test?.description,
|
|
80
81
|
entries,
|
|
81
82
|
};
|
|
82
83
|
}
|
|
@@ -85,24 +86,18 @@ export async function GET(req: Request) {
|
|
|
85
86
|
const url = new URL(req.url);
|
|
86
87
|
const id = url.searchParams.get('id');
|
|
87
88
|
|
|
88
|
-
// Single-connector detail
|
|
89
89
|
if (id) {
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
return NextResponse.json({ connector: toConnectorPayload(def) });
|
|
90
|
+
const inst = getInstalledConnector(id);
|
|
91
|
+
if (!inst) return NextResponse.json({ error: 'connector not found' }, { status: 404 });
|
|
92
|
+
return NextResponse.json({ connector: toConnectorPayload(inst.definition, inst.config, true) });
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.map(
|
|
103
|
-
.filter((d): d is PluginDefinition => !!d)
|
|
104
|
-
.filter(d => !onlyInstalled || !!getInstalledPlugin(d.id))
|
|
105
|
-
.map(toConnectorPayload);
|
|
95
|
+
// The extension always wants the installed list — there is no "not
|
|
96
|
+
// installed but visible" path on this endpoint anymore (that lives
|
|
97
|
+
// in /api/connectors/marketplace, coming in P1.6).
|
|
98
|
+
const connectors = listInstalledConnectors()
|
|
99
|
+
.filter(c => c.enabled)
|
|
100
|
+
.map(c => toConnectorPayload(c.definition, c.config, true));
|
|
106
101
|
|
|
107
102
|
return NextResponse.json({ connectors });
|
|
108
103
|
}
|
package/app/api/jobs/route.ts
CHANGED
|
@@ -30,6 +30,7 @@ export async function POST(req: Request) {
|
|
|
30
30
|
dedup_field: String(body.dedup_field),
|
|
31
31
|
dispatch_type: body.dispatch_type,
|
|
32
32
|
dispatch_params: body.dispatch_params,
|
|
33
|
+
skills: Array.isArray(body.skills) ? body.skills.map((s: unknown) => String(s)) : [],
|
|
33
34
|
mark_existing_as_seen: body.mark_existing_as_seen !== false,
|
|
34
35
|
});
|
|
35
36
|
return NextResponse.json({ job });
|
package/app/api/monitor/route.ts
CHANGED
|
@@ -22,8 +22,25 @@ export async function GET() {
|
|
|
22
22
|
const terminal = countProcess('terminal-standalone');
|
|
23
23
|
const telegram = countProcess('telegram-standalone');
|
|
24
24
|
const workspace = countProcess('workspace-standalone');
|
|
25
|
+
const chat = countProcess('chat-standalone');
|
|
26
|
+
const browserBridge = countProcess('browser-bridge-standalone');
|
|
25
27
|
const tunnel = countProcess('cloudflared tunnel');
|
|
26
28
|
|
|
29
|
+
// Chat backend health (port 8408 — process can be alive but crashed)
|
|
30
|
+
let chatStatus: { running: boolean; sessions: number; port: number } = {
|
|
31
|
+
running: false,
|
|
32
|
+
sessions: 0,
|
|
33
|
+
port: Number(process.env.CHAT_PORT) || 8408,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const chatRes = run(`curl -s http://127.0.0.1:${chatStatus.port}/api/status 2>/dev/null`);
|
|
37
|
+
if (chatRes) {
|
|
38
|
+
const data = JSON.parse(chatRes);
|
|
39
|
+
chatStatus.running = true;
|
|
40
|
+
chatStatus.sessions = data.active_sse_sessions ?? 0;
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
27
44
|
// MCP Server (runs inside workspace process, check /health endpoint)
|
|
28
45
|
let mcpStatus = { running: false, sessions: 0 };
|
|
29
46
|
try {
|
|
@@ -65,6 +82,15 @@ export async function GET() {
|
|
|
65
82
|
terminal: { running: terminal.count > 0, pid: terminal.pid, startedAt: terminal.startedAt },
|
|
66
83
|
telegram: { running: telegram.count > 0, pid: telegram.pid, startedAt: telegram.startedAt },
|
|
67
84
|
workspace: { running: workspace.count > 0, pid: workspace.pid, startedAt: workspace.startedAt },
|
|
85
|
+
browserBridge: { running: browserBridge.count > 0, pid: browserBridge.pid, startedAt: browserBridge.startedAt },
|
|
86
|
+
chat: {
|
|
87
|
+
running: chatStatus.running,
|
|
88
|
+
pid: chat.pid,
|
|
89
|
+
startedAt: chat.startedAt,
|
|
90
|
+
port: chatStatus.port,
|
|
91
|
+
sessions: chatStatus.sessions,
|
|
92
|
+
processAlive: chat.count > 0,
|
|
93
|
+
},
|
|
68
94
|
tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl, startedAt: tunnel.startedAt },
|
|
69
95
|
mcp: { running: mcpStatus.running, port: 8406, sessions: mcpStatus.sessions },
|
|
70
96
|
},
|