@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.
Files changed (45) hide show
  1. package/RELEASE_NOTES.md +6 -6
  2. package/app/api/connectors/[id]/settings/route.ts +31 -37
  3. package/app/api/connectors/[id]/test/route.ts +260 -0
  4. package/app/api/connectors/install-local/route.ts +211 -0
  5. package/app/api/connectors/marketplace/route.ts +79 -0
  6. package/app/api/connectors/route.ts +41 -46
  7. package/app/api/jobs/route.ts +4 -0
  8. package/app/api/skills/install-local/route.ts +282 -0
  9. package/components/ConnectorsPanel.tsx +526 -211
  10. package/components/SettingsModal.tsx +1 -0
  11. package/components/SkillsPanel.tsx +42 -1
  12. package/lib/agents/claude-adapter.ts +4 -0
  13. package/lib/agents/types.ts +6 -0
  14. package/lib/chat/agent-loop.ts +13 -22
  15. package/lib/chat/protocols/http.ts +1 -1
  16. package/lib/chat/protocols/shell.ts +1 -1
  17. package/lib/chat/tool-dispatcher.ts +20 -20
  18. package/lib/connectors/migration.ts +110 -0
  19. package/lib/connectors/registry.ts +328 -0
  20. package/lib/connectors/sync.ts +305 -0
  21. package/lib/connectors/types.ts +253 -0
  22. package/lib/help-docs/00-overview.md +1 -0
  23. package/lib/help-docs/17-connectors.md +241 -189
  24. package/lib/help-docs/21-build-connector.md +314 -0
  25. package/lib/help-docs/CLAUDE.md +4 -2
  26. package/lib/init.ts +25 -0
  27. package/lib/jobs/dispatcher.ts +28 -8
  28. package/lib/jobs/scheduler.ts +66 -6
  29. package/lib/jobs/store.ts +51 -2
  30. package/lib/jobs/types.ts +32 -0
  31. package/lib/pipeline-scheduler.ts +3 -2
  32. package/lib/pipeline.ts +137 -15
  33. package/lib/plugins/registry.ts +9 -42
  34. package/lib/plugins/types.ts +4 -129
  35. package/lib/settings.ts +7 -0
  36. package/lib/skills.ts +27 -1
  37. package/lib/task-manager.ts +62 -2
  38. package/package.json +4 -1
  39. package/src/core/db/database.ts +4 -0
  40. package/lib/builtin-plugins/github-api.yaml +0 -93
  41. package/lib/builtin-plugins/gitlab.yaml +0 -860
  42. package/lib/builtin-plugins/mantis.probe.js +0 -176
  43. package/lib/builtin-plugins/mantis.yaml +0 -964
  44. package/lib/builtin-plugins/pmdb.yaml +0 -178
  45. package/lib/builtin-plugins/teams.yaml +0 -913
@@ -1,16 +1,72 @@
1
1
  # Connectors
2
2
 
3
- **Connectors** are Forge plugins of `category: connector` that expose tool
4
- schemas + **the extraction scripts that implement them** to the **Forge
5
- browser extension**. The extension is a generic runner: it doesn't know
6
- about Mantis or GitLab specificallyit loads the manifest, finds/opens
7
- the right tab, and executes whatever script the manifest ships.
8
-
9
- > **Architectural principle**: adding a new connector should be a
10
- > Forge-only change. No browser extension release required.
11
- >
12
- > See `docs/Connector-DeclarativeExtract-Spec.md` for the full spec of
13
- > the manifest schema and runtime contract.
3
+ **Connectors** expose external services (Mantis, GitLab, Teams, …) as
4
+ tools that the Forge chat agent and the browser extension can call.
5
+ Each connector is a declarative YAML manifest schema + the extraction
6
+ scripts that implement each toolpulled from the remote
7
+ [`forge-connectors`](https://github.com/aiwatching/forge-connectors)
8
+ registry.
9
+
10
+ > **Architecture**: connectors are an independent subsystem (`lib/connectors/`).
11
+ > They are NOT plugins. Pipeline-node plugins (`lib/plugins/`) and connectors
12
+ > share no schema, no registry, no config file.
13
+
14
+ There are **no built-in connectors**. A fresh install starts with an empty
15
+ connector list; the user picks what they want from the marketplace.
16
+
17
+ ## Marketplace flow
18
+
19
+ 1. On boot, Forge fetches `registry.json` from `connectorsRepoUrl`
20
+ (default `https://raw.githubusercontent.com/aiwatching/forge-connectors/main`)
21
+ and caches it at `<dataDir>/connectors/registry-cache.json`.
22
+ 2. The Settings → Connectors panel reads the cache and lists every
23
+ available connector with its install state:
24
+ - **Available** — in registry, not installed
25
+ - **Installed v0.5.0** — manifest on disk, config row present
26
+ - **Update 0.5 → 0.6** — registry has newer version
27
+ 3. Clicking **Install** fetches `<id>/manifest.yaml` from the registry,
28
+ writes it to `<dataDir>/connectors/<id>/manifest.yaml`, and adds a
29
+ row to `<dataDir>/connector-configs.json`.
30
+ 4. The user fills in settings (host URL, PAT, etc.) via the existing
31
+ settings UI in the extension or `/api/connectors/<id>/settings`.
32
+ 5. Chat tools become available the instant the manifest lands on disk —
33
+ no Forge restart.
34
+
35
+ Sync re-runs every hour and on manual **Refresh** in the Settings panel.
36
+ Network failures are silent — the cache from the last successful sync
37
+ keeps the marketplace usable offline.
38
+
39
+ ## Repository layout
40
+
41
+ The `forge-connectors` repo (forkable via `connectorsRepoUrl`) is:
42
+
43
+ ```
44
+ registry.json
45
+ <id>/
46
+ manifest.yaml ← what Forge installs
47
+ README.md ← optional, surfaced in the marketplace
48
+ ```
49
+
50
+ `registry.json` lists every connector with id/name/version/icon/description
51
+ + `min_forge_version`. Forge only consults the registry for the install
52
+ flow; once installed, `<dataDir>/connectors/<id>/manifest.yaml` is the
53
+ source of truth for that user.
54
+
55
+ ## Local data layout
56
+
57
+ ```
58
+ <dataDir>/
59
+ ├── connectors/
60
+ │ ├── registry-cache.json last fetch from forge-connectors
61
+ │ ├── mantis/manifest.yaml installed manifest copy
62
+ │ ├── gitlab/manifest.yaml
63
+ │ └── …
64
+ └── connector-configs.json { "<id>": { config, installed_version, enabled } }
65
+ ```
66
+
67
+ Secret fields (`type: secret`) in `config` are encrypted at rest with
68
+ AES-256-GCM via the existing key at `<dataDir>/.encrypt-key`. The on-wire
69
+ GET response masks them with bullets so plaintext never leaves the server.
14
70
 
15
71
  ## Execution model: DOM extraction, not REST
16
72
 
@@ -19,72 +75,56 @@ user's tabs and parse rendered HTML via `chrome.scripting.executeScript`.
19
75
  This is the whole point of routing through a browser extension — reuse
20
76
  the user's already-authenticated UI session, with zero token management.
21
77
 
22
- Why this matters in practice:
23
-
24
78
  | System | REST API needs | Browser DOM needs |
25
79
  |---|---|---|
26
- | MantisBT | Per-user API token (`Authorization` header) | Already-logged-in PHPSESSID cookie ✅ |
27
- | GitLab | Personal Access Token | Session cookie on `/-/issues/` etc. ✅ |
80
+ | MantisBT | Per-user API token | Already-logged-in PHPSESSID cookie ✅ |
81
+ | GitLab UI | Personal Access Token | Session cookie on `/-/issues/` etc. ✅ |
28
82
  | JIRA Server | PAT or basic auth | Session cookie ✅ |
29
83
  | Teams web | MSAL bearer token | Session cookie ✅ |
30
84
 
31
- If a connector hit the REST API path, every user would need to mint and
32
- manage a token per system. That defeats the extension's value
33
- proposition. **Token-based fallback is opt-in per connector, not the
34
- default code path.**
35
-
36
85
  This means:
37
- - **No PAT / API tokens to manage** — connector reuses the user's logged-in browser session.
38
- - **Data never flows through Forge** — Forge only ships the manifest + extraction script. The LLM in the extension calls the tool, the extension scrapes in the user's tab, the LLM sees the result.
39
- - Forge's role: **discovery + settings sync + script delivery**.
40
- - Trade-off: site redesigns break scripts. Bump the manifest `version` and update the `script` block — users get the fix on next refresh, no extension release.
41
86
 
42
- ## Architecture
87
+ - **No PAT / API tokens to manage** for browser connectors — they reuse
88
+ the user's logged-in session.
89
+ - **Data never flows through Forge** — Forge only ships the manifest +
90
+ extraction script. The LLM in the chat backend calls the tool, the
91
+ extension scrapes in the user's tab, the LLM sees the result.
92
+ - Trade-off: site redesigns break scripts. Bump the manifest `version`
93
+ in the registry and users pull the fix on next sync — no Forge or
94
+ extension release needed.
43
95
 
44
- ```
45
- Forge Extension User's tabs
46
- ───── ───────── ───────────
47
- mantis.yaml ┌──────────────┐
48
- tools: Generic runner │ Mantis tab │
49
- list_my_bugs: ─────────▶ ① fetch manifest │ (logged in) │
50
- page: { url, ... } ② render templates │ │
51
- script: | ③ acquire tab matching │ #bug_list │
52
- const list = ... host_match, navigate │ ◀── runs │
53
- return { bugs, ... } to page.url the script
54
- ④ executeScript(script) from the
55
- in the tab manifest
56
- ⑤ return JSON to LLM │ │
57
- └──────────────┘
58
- ```
59
-
60
- The extension has **no per-connector code**. Adding GitLab means dropping
61
- a `gitlab.yaml` next to `mantis.yaml` — extension auto-discovers it.
62
-
63
- ## Plugin manifest format
64
-
65
- A connector plugin is a YAML in `lib/builtin-plugins/<id>.yaml` (or
66
- `~/.forge/plugins/<id>/plugin.yaml`):
96
+ ## Manifest format
67
97
 
68
98
  ```yaml
69
99
  id: mantis
70
100
  name: MantisBT
71
101
  icon: "🐞"
72
- version: "0.2.0"
73
- category: connector # ← marks this as a connector
74
- mode: browser-side # server-side | browser-side | hybrid
102
+ version: "0.5.0"
103
+ author: forge
104
+ description: |
105
+ Read + comment on MantisBT bugs from the user's logged-in browser session.
75
106
 
76
- # Per-user settings (rendered as a form in the extension)
107
+ min_forge_version: "0.8.0"
108
+
109
+ # Default extension execution context.
110
+ # main — permissive-CSP sites (Mantis, GitLab self-hosted)
111
+ # isolated — strict-CSP sites that block eval (Teams, github.com)
112
+ runner: main
113
+
114
+ # Per-user settings (rendered as a form in the extension).
77
115
  settings:
78
116
  base_url:
79
117
  type: string
80
118
  label: Mantis base URL
81
119
  required: true
120
+ token:
121
+ type: secret # encrypted at rest (AES-256-GCM)
122
+ label: API token (optional fallback)
82
123
 
83
- # Plugin-level: where the extension finds / opens an authenticated tab.
84
- # Chrome match pattern; {settings.*} expanded at API response time.
124
+ # Chrome match pattern; {settings.*} expanded server-side.
85
125
  host_match: "{base_url}/*"
86
126
 
87
- # Substring detected after navigation → tells the runner "user not logged in".
127
+ # Substring detected after navigation → "user not logged in".
88
128
  login_redirect: "/login_page.php"
89
129
 
90
130
  tools:
@@ -94,22 +134,19 @@ tools:
94
134
  status: { type: select, options: ["open", "closed", "all"], default: "open" }
95
135
  limit: { type: number, default: 50 }
96
136
 
97
- # Page to navigate to (or stay on, if on_target matches current URL).
137
+ # Default protocol is 'browser' runs in the user's tab.
98
138
  page:
99
139
  url: "{base_url}/view_all_bug_page.php"
100
140
  on_target: "/view_all_bug_page.php"
101
-
102
- # Function BODY. Implicit `args` parameter. Returns JSON-serializable.
103
- # Runs IN THE USER'S TAB (page context). No closures over Forge / extension.
104
141
  script: |
105
142
  const { status, limit } = args;
106
143
  const list = document.querySelector('#bug_list');
107
144
  if (!list) return { bugs: [], total: 0, _error: '#bug_list not found' };
108
- // ... extract rows, filter, return
145
+ // extract rows, filter, return
109
146
  return { bugs, total };
110
147
 
111
148
  add_comment:
112
- destructive: true # extension prompts user before running
149
+ destructive: true # extension prompts before running
113
150
  description: "Add a note to a bug."
114
151
  parameters:
115
152
  bug_id: { type: number, required: true }
@@ -118,78 +155,104 @@ tools:
118
155
  url: "{base_url}/bug_view_page.php?bug_id={args.bug_id}"
119
156
  script: |
120
157
  // Form-submit or fetch() with same-origin cookies
121
- ...
158
+
122
159
  ```
123
160
 
124
161
  **Template variables**:
125
162
  - `{base_url}` / `{settings.<name>}` → expanded server-side from saved settings
126
- - `{args.<name>}` → expanded by the extension at run time from the LLM's tool input
163
+ - `{args.<name>}` → expanded by the runtime from the LLM's tool input
127
164
 
128
- **Script contract**:
165
+ **`script` contract**:
129
166
  - Receives `args` (the LLM's parameters)
130
167
  - Returns a JSON-serializable value (no DOM nodes, no functions)
131
168
  - Has access to `document`, `fetch`, `URL`, etc. (page context)
132
- - Can call same-origin `fetch()` — cookies auto-attached
133
- - Must be self-contained — no closures over the manifest's surroundings
134
- - Errors thrown are caught by the runner and returned as tool errors
169
+ - Same-origin `fetch()` — cookies auto-attached
170
+ - Self-contained — no closures over the manifest's surroundings
171
+ - Uncaught throws become tool errors
135
172
 
136
- See `docs/Connector-DeclarativeExtract-Spec.md` for the complete contract.
173
+ ## Server-side protocols (`http`, `shell`)
137
174
 
138
- ### 1 plugin = 1 connector (default)
175
+ When a service has a clean REST API and you don't need a browser tab at
176
+ all, declare `protocol: http` on the tool. Forge issues the request
177
+ server-side and returns the body to the LLM. Same for `protocol: shell`
178
+ — Forge spawns a process with an explicit arg array (no `shell:true`,
179
+ so templated values cannot inject metacharacters).
139
180
 
140
- The above shape is the **default 1:1 case**. For same-vendor suites with
141
- shared auth (Atlassian, Google Workspace, M365), use the `connectors[]`
142
- escape hatch — one plugin declares multiple connector entries that share
143
- the user's OAuth/SSO:
181
+ ```yaml
182
+ tools:
183
+ get_repo:
184
+ protocol: http
185
+ parameters:
186
+ repo: { type: string, required: true }
187
+ request:
188
+ method: GET
189
+ url: 'https://api.github.com/repos/{args.repo}'
190
+ headers:
191
+ Accept: 'application/vnd.github+json'
192
+ Authorization: 'Bearer {settings.token}'
193
+
194
+ git_log:
195
+ protocol: shell
196
+ parameters:
197
+ repo: { type: string, required: true }
198
+ n: { type: number }
199
+ command: ['git', '-C', '{args.repo}', 'log', '-n', '{args.n}', '--oneline']
200
+ timeout_ms: 5000
201
+ ```
202
+
203
+ Response body / stdout is truncated to ~8 KB for the LLM context;
204
+ default timeout 30 s, max 5 min. `protocol: browser` (the default) runs
205
+ in the extension; `http` and `shell` run entirely on Forge (chat-standalone,
206
+ port 8408).
207
+
208
+ **Safety:** `protocol: shell` lets a manifest invoke any binary on PATH.
209
+ Always read the script before installing.
210
+
211
+ ## 1:1 vs 1:N suites
212
+
213
+ The default is **one connector = one tool surface** (Mantis, GitLab,
214
+ Teams, …). For same-vendor suites with shared auth (Atlassian, Google
215
+ Workspace, M365), a manifest can declare multiple `connectors[]`
216
+ entries that share user settings:
144
217
 
145
218
  ```yaml
146
219
  id: atlassian-suite
147
- category: connector
148
- mode: hybrid
149
220
  connectors:
150
221
  - id: jira
151
222
  host_match: "*://*.atlassian.net/*"
152
- tools: { ... }
223
+ tools: { }
153
224
  - id: confluence
154
225
  host_match: "*://*.atlassian.net/wiki/*"
155
- tools: { ... }
226
+ tools: { }
156
227
  ```
157
228
 
158
- Most plugins should stay 1:1. Use 1:N only for genuine shared-auth suites.
229
+ Most manifests should stay 1:1. Use 1:N only for genuine shared-auth suites.
159
230
 
160
231
  ## HTTP API
161
232
 
162
- All endpoints require `X-Forge-Token` header (obtain via `POST /api/auth/verify`).
233
+ All endpoints require `X-Forge-Token` (obtain via `POST /api/auth/verify`).
163
234
 
164
235
  ### `GET /api/connectors`
165
- List connector plugins (installed + available). For installed connectors,
166
- `page.url` / `host_match` etc. have `{base_url}` / `{settings.*}` expanded
167
- from the user's saved settings; `{args.*}` is left literal.
236
+ List installed connectors. `page.url` / `host_match` etc. are expanded
237
+ from the user's settings; `{args.*}` stays literal for the runtime.
168
238
 
169
239
  ```json
170
240
  {
171
241
  "connectors": [
172
242
  {
173
- "plugin_id": "mantis",
243
+ "plugin_id": "mantis", /* legacy wire-name; equals id */
174
244
  "name": "MantisBT",
175
245
  "icon": "🐞",
176
- "version": "0.2.0",
177
- "mode": "browser-side",
246
+ "version": "0.5.0",
178
247
  "installed": true,
179
248
  "host_match": "https://mantis.acme.com/*",
180
249
  "login_redirect": "/login_page.php",
250
+ "runner": "main",
181
251
  "entries": [
182
252
  {
183
253
  "id": "mantis",
184
- "tools": {
185
- "list_my_bugs": {
186
- "description": "...",
187
- "parameters": {...},
188
- "page": { "url": "https://mantis.acme.com/view_all_bug_page.php", "on_target": "/view_all_bug_page.php" },
189
- "script": "const { status, limit } = args; ..."
190
- }
191
- },
192
- "settings": { "base_url": {...} }
254
+ "tools": { … },
255
+ "settings": { "base_url": { … } }
193
256
  }
194
257
  ]
195
258
  }
@@ -197,126 +260,115 @@ from the user's saved settings; `{args.*}` is left literal.
197
260
  }
198
261
  ```
199
262
 
200
- Query params:
201
- - `installed=true` only plugins the user has configured
202
- - `id=<plugin_id>` → single connector detail (alternative to a path param)
203
-
204
- ### `GET /api/connectors?id=<id>` / `GET /api/connectors/<id>`
205
- Single connector detail. Same shape as a list entry, wrapped in `{ connector: ... }`.
263
+ ### `GET /api/connectors?id=<id>`
264
+ Single connector detail, wrapped in `{ connector: }`.
206
265
 
207
266
  ### `GET /api/connectors/<id>/settings`
208
- Read the user's saved settings for a connector.
267
+ Read the user's saved settings. Secret fields come back as bullets.
209
268
 
210
269
  ```json
211
270
  {
212
- "settings": { "base_url": "https://mantis.acme.com", "default_project": "Web" },
213
- "schema": { "base_url": {...}, "default_project": {...} },
271
+ "settings": { "base_url": "https://mantis.acme.com", "token": "••••••••" },
272
+ "schema": { "base_url": {}, "token": { "type": "secret", … } },
214
273
  "installed": true
215
274
  }
216
275
  ```
217
276
 
218
- `schema` is the merged settings schema across all entries (1:N). `settings`
219
- includes any defaults declared in the schema, overlaid with the user's
220
- saved values.
221
-
222
277
  ### `POST /api/connectors/<id>/settings`
223
- Save settings. Body: `{ settings: { base_url: "..." } }` (or the object directly).
278
+ Save settings. If the client sends `••••••••` for a secret, the stored
279
+ value is preserved (so editing one field doesn't wipe the PAT).
224
280
 
225
- First save = creates the install record. Subsequent saves update it.
281
+ ### `GET /api/connectors/marketplace`
282
+ List registry entries merged with installed state.
226
283
 
227
284
  ```json
228
- { "ok": true, "settings": { "base_url": "..." } }
285
+ {
286
+ "fetched_at": "2026-05-19T16:31:02.000Z",
287
+ "base_url": "https://raw.githubusercontent.com/aiwatching/forge-connectors/main",
288
+ "entries": [
289
+ { "id": "mantis", "version": "0.5.0", "installed_version": "0.5.0", "update_available": false, … },
290
+ { "id": "gitlab", "version": "1.1.0", "installed_version": "1.0.0", "update_available": true, … },
291
+ { "id": "teams", "version": "0.8.1", /* available */ … }
292
+ ]
293
+ }
229
294
  ```
230
295
 
231
- ## Built-in connectors
296
+ ### `POST /api/connectors/marketplace`
297
+ Marketplace actions.
232
298
 
233
- | ID | Description |
299
+ | `action` | Effect |
234
300
  |---|---|
235
- | `mantis` | MantisBT list/get/search bugs, add comments. Self-hosted. |
236
- | `gitlab-browser` | GitLab issues, MRs, pipelines via browser session. gitlab.com + self-hosted. |
237
-
238
- The CLI-backed `glab` GitLab integration in `gitlab-issue-fix-and-review`
239
- is **separate** that runs server-side as a pipeline. The browser
240
- connector is for the extension's interactive use.
241
-
242
- ## Writing a new connector
243
-
244
- 1. Drop a YAML in `~/.forge/plugins/<id>/plugin.yaml` with `category: connector`.
245
- 2. Define `settings` (e.g. `base_url`) the user must provide.
246
- 3. Define `host_match` and `login_redirect` at the plugin level.
247
- 4. For each tool: schema (`parameters`), `page` block, `script` body.
248
- 5. **Use DOM extraction**, not REST API calls. `script` runs in the tab,
249
- has same-origin cookies via `fetch()`, can read the DOM directly.
250
- 6. Use `mantis.probe.js`-style helper scripts at dev time to discover
251
- stable selectors; paste the resulting selectors into your `script`.
252
- 7. Restart Forge — extension picks it up via `GET /api/connectors` on
253
- next refresh. **No extension code changes needed.**
254
-
255
- ### When REST is unavoidable
256
-
257
- Some sites are pure SPAs that virtualize lists, render via canvas, or
258
- require many interactions to expose data. In those rare cases, add an
259
- optional `api_token` setting (`type: secret`) and call `fetch()` with
260
- the token from within `script`. Default code path stays DOM; token is
261
- the fallback the user explicitly accepts.
262
-
263
- ## Server-side protocols (http, shell)
264
-
265
- When a site has a clean REST API and you don't need a browser tab at
266
- all, declare `protocol: http` on the tool. Forge issues the request
267
- server-side and returns the body to the LLM. Same for `protocol:
268
- shell` — Forge spawns a process with an explicit arg array (no
269
- `shell:true`, so templated values cannot inject metacharacters).
270
-
271
- ```yaml
272
- # lib/builtin-plugins/github-api.yaml — see file for full example
273
- tools:
274
- get_repo:
275
- protocol: http
276
- parameters:
277
- repo: { type: string, required: true }
278
- request:
279
- method: GET
280
- url: 'https://api.github.com/repos/{args.repo}'
281
- headers:
282
- Accept: 'application/vnd.github+json'
283
- Authorization: 'Bearer {settings.token}'
284
-
285
- git_log:
286
- protocol: shell
287
- parameters:
288
- repo: { type: string, required: true }
289
- n: { type: number }
290
- command: ['git', '-C', '{args.repo}', 'log', '-n', '{args.n}', '--oneline']
291
- timeout_ms: 5000
301
+ | `sync` | Re-fetch `registry.json` (and refresh installed manifests with `refreshInstalled: true`) |
302
+ | `install` | Fetch the manifest for `id` and write it to `<dataDir>/connectors/<id>/` |
303
+ | `update` | Alias for `install` — pulls the latest manifest |
304
+ | `uninstall` | Drop the manifest. `keepConfig: true` (default) preserves the user's settings + PAT |
305
+ | `status` | Probe a single `id`: returns `{ installed, installed_version, enabled }` |
306
+
307
+ ## Authoring a new connector
308
+
309
+ Three ways to land a custom connector:
310
+
311
+ 1. **Local-only (fastest)** write a `manifest.yaml`, upload it via
312
+ the "+ Upload" button in Settings Connectors (or drag-drop). For
313
+ multi-file connectors, zip it with `manifest.yaml` at the root.
314
+ See `21-build-connector.md` for the manifest schema and an
315
+ interview-style template.
316
+ 2. **Forge Help AI** ask Forge chat "build me a connector for X"; the
317
+ AI walks through requirements, writes the manifest to
318
+ `<dataDir>/connectors/<id>/manifest.yaml`, and registers it via
319
+ `POST /api/connectors/install-local`.
320
+ 3. **Share via forge-connectors** — fork
321
+ [`forge-connectors`](https://github.com/aiwatching/forge-connectors),
322
+ add `<id>/manifest.yaml`, add an entry to `registry.json`, open a PR.
323
+ Maintainers review the script bodies before merge.
324
+
325
+ Iterating on a private registry: point `settings.connectorsRepoUrl` at
326
+ a private fork.
327
+
328
+ ### POST /api/connectors/install-local
329
+
330
+ Used by both the Upload button and the Help AI:
331
+
332
+ ```http
333
+ POST /api/connectors/install-local
334
+ Content-Type: application/json
335
+ X-Forge-Token:
336
+
337
+ { "yaml": "id: my-jira\nname: MyJira\nversion: \"0.1.0\"\n..." }
292
338
  ```
293
339
 
294
- Templates `{base_url}`, `{settings.*}`, `{args.*}` all expand at
295
- dispatch time. For `http`, body can be a string or a JSON object; for
296
- `shell`, every arg is templated independently so an arg with spaces or
297
- quotes stays a single literal arg.
340
+ or `multipart/form-data` with a `file` field accepting `.yaml`,
341
+ `.yml`, or `.zip`. Zip must have `manifest.yaml` at the root.
298
342
 
299
- Response body / stdout is truncated to ~8 KB for the LLM context;
300
- default timeout 30 s, max 5 min. `protocol: browser` (the default)
301
- keeps using the extension runner; `http` and `shell` run entirely on
302
- Forge (chat-standalone, port 8408).
343
+ ## Migration from pre-v0.9
303
344
 
304
- **Safety:** `protocol: shell` lets a YAML pick any binary on PATH —
305
- review at install time. There is no auto allow-list in v1.
345
+ Pre-v0.9 Forge stored connectors as built-in plugins under
346
+ `lib/builtin-plugins/<id>.yaml` with `category: connector`. On upgrade:
306
347
 
307
- ### Iterating selectors
348
+ 1. `migrateConnectorConfigs()` runs once per boot. For each known id
349
+ (mantis, gitlab, teams, pmdb, github-api), if there's a row in
350
+ `plugin-configs.json`, the encrypted config blob is moved to
351
+ `connector-configs.json` and the old row is removed.
352
+ 2. The next `syncRegistry({ refreshInstalled: true })` pulls the
353
+ matching manifest from `forge-connectors` and writes it to
354
+ `<dataDir>/connectors/<id>/manifest.yaml`.
355
+ 3. Chat tools come back online ~1 s after migration completes. PAT
356
+ and base URL are preserved across the upgrade.
308
357
 
309
- If a connector breaks after a site redesign, **only the YAML's `script`
310
- body needs to change**. Bump `version`, edit, restart Forge extension
311
- users pick up the fix on their next refresh.
358
+ If the user is offline at upgrade time, the marketplace will be empty
359
+ until the network returns; config rows remain, so the next sync
360
+ restores everything without re-entering credentials.
312
361
 
313
362
  ## Troubleshooting
314
363
 
315
364
  | Symptom | Cause + fix |
316
365
  |---|---|
317
- | Extension shows "no connectors" | Token expired call `POST /api/auth/verify` to refresh |
318
- | Tool not appearing for LLM | Plugin loaded but `category` missing, OR `script` missing for the tool |
319
- | Settings POST returns 500 | Plugin id not found (typo?) or `plugin-configs.json` not writable |
320
- | Tool returns "login required" | `login_redirect` substring matched the tab URL after nav user needs to log in to that site |
321
- | Script throws but error opaque | Wrap script body in try/catch and `return { _error: e.message }`; the runner catches uncaught throws but caller-visible message is cleaner |
322
- | Strict-CSP site (github.com etc.) refuses `new Function` | Page CSP blocks dynamic eval. For those sites, write a server-side connector (REST + token) instead of browser-side. Document the limitation per connector. |
366
+ | Marketplace is empty | Initial sync failed (offline / bad URL). Click **Refresh** in Settings → Connectors. |
367
+ | `Sync error: self-signed certificate in certificate chain` | Corporate TLS-interception proxy (Zscaler, Palo Alto, etc.). Node doesn't trust the proxy's root CA by default. Export `NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem` before starting Forge, or run `forge server restart` with that env var set. macOS users can extract the bundle from Keychain Access → System Roots. |
368
+ | `Sync error: ENOTFOUND` | DNS can't resolve `raw.githubusercontent.com` VPN / firewall blocking. Point `connectorsRepoUrl` at a private mirror, or sync manifests manually into `<dataDir>/connectors/<id>/manifest.yaml`. |
369
+ | Installed connector missing from chat | Manifest deleted but config row remains. Re-install from the marketplace. |
370
+ | "Update available" badge stuck | Network failure on the refresh. Re-sync via **Refresh**. |
371
+ | Tool not appearing for the LLM | Connector installed but disabled. Toggle it on in the Settings panel. |
372
+ | Tool returns "login required" | `login_redirect` substring matched the tab URL after nav — user needs to log in to that site. |
373
+ | Strict-CSP site refuses `new Function` | Page CSP blocks dynamic eval. Set `runner: isolated` in the manifest, or rewrite the tool as `protocol: http`. |
374
+ | `POST /api/connectors/<id>/settings` returns 404 | Manifest not on disk (`<dataDir>/connectors/<id>/manifest.yaml`). Install the connector first. |