@aion0/forge 0.6.1 → 0.8.0

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 (145) hide show
  1. package/.forge/mcp.json +8 -0
  2. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
  3. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
  4. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
  5. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
  6. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
  7. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
  8. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
  9. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
  10. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
  11. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
  12. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
  13. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
  14. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
  15. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
  16. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
  17. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
  18. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
  19. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
  20. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
  21. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
  22. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
  23. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
  24. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
  25. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
  26. package/CLAUDE.md +2 -2
  27. package/RELEASE_NOTES.md +101 -5
  28. package/app/api/auth/check/route.ts +18 -0
  29. package/app/api/browser-bridge/route.ts +70 -0
  30. package/app/api/chat/sessions/[id]/events/route.ts +17 -0
  31. package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
  32. package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
  33. package/app/api/chat/sessions/[id]/route.ts +23 -0
  34. package/app/api/chat/sessions/route.ts +12 -0
  35. package/app/api/chat/temper-ping/route.ts +18 -0
  36. package/app/api/chat-proxy/[...path]/route.ts +83 -0
  37. package/app/api/connector-tool/route.ts +38 -0
  38. package/app/api/connectors/[id]/settings/route.ts +112 -0
  39. package/app/api/connectors/route.ts +108 -0
  40. package/app/api/health/tools/route.ts +14 -0
  41. package/app/api/issue-scanner-gitlab/route.ts +95 -0
  42. package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
  43. package/app/api/jobs/[id]/route.ts +31 -0
  44. package/app/api/jobs/[id]/run/route.ts +44 -0
  45. package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
  46. package/app/api/jobs/[id]/runs/route.ts +15 -0
  47. package/app/api/jobs/preview/route.ts +193 -0
  48. package/app/api/jobs/route.ts +36 -0
  49. package/app/api/notify/test/route.ts +39 -7
  50. package/app/api/pipelines/[id]/route.ts +10 -1
  51. package/app/api/pipelines/route.ts +16 -2
  52. package/app/api/plugins/route.ts +40 -8
  53. package/app/api/project-sessions/route.ts +50 -10
  54. package/app/api/settings/route.ts +13 -0
  55. package/app/chat/page.tsx +531 -0
  56. package/bin/forge-server.mjs +3 -1
  57. package/cli/chat.ts +283 -0
  58. package/cli/jobs.ts +176 -0
  59. package/cli/mw.ts +28 -1
  60. package/cli/worktree.ts +245 -0
  61. package/components/ConnectorsPanel.tsx +275 -0
  62. package/components/Dashboard.tsx +90 -37
  63. package/components/JobsView.tsx +361 -0
  64. package/components/LogViewer.tsx +12 -2
  65. package/components/PipelineView.tsx +275 -56
  66. package/components/PluginsPanel.tsx +3 -1
  67. package/components/SettingsModal.tsx +229 -40
  68. package/components/SkillsPanel.tsx +12 -4
  69. package/components/TerminalLauncher.tsx +3 -1
  70. package/components/WebTerminal.tsx +32 -9
  71. package/components/WorkspaceView.tsx +18 -10
  72. package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
  73. package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
  74. package/docs/Implementation-Plan-Browser-Agent.md +487 -0
  75. package/docs/Jobs-Design.md +240 -0
  76. package/docs/LOCAL-DEPLOY.md +3 -3
  77. package/docs/RFC-Browser-Connectors.md +509 -0
  78. package/lib/agents/index.ts +44 -6
  79. package/lib/agents/types.ts +1 -1
  80. package/lib/browser-bridge-standalone.ts +317 -0
  81. package/lib/builtin-plugins/github-api.yaml +93 -0
  82. package/lib/builtin-plugins/gitlab.yaml +860 -0
  83. package/lib/builtin-plugins/mantis.probe.js +176 -0
  84. package/lib/builtin-plugins/mantis.yaml +964 -0
  85. package/lib/builtin-plugins/pmdb.yaml +178 -0
  86. package/lib/builtin-plugins/teams.yaml +913 -0
  87. package/lib/chat/__test__/smoke.ts +30 -0
  88. package/lib/chat/agent-loop.ts +523 -0
  89. package/lib/chat/bridge-client.ts +59 -0
  90. package/lib/chat/llm/anthropic.ts +99 -0
  91. package/lib/chat/llm/index.ts +20 -0
  92. package/lib/chat/llm/openai.ts +215 -0
  93. package/lib/chat/llm/types.ts +42 -0
  94. package/lib/chat/local-memory.ts +300 -0
  95. package/lib/chat/memory-store.ts +87 -0
  96. package/lib/chat/memory-tools.ts +157 -0
  97. package/lib/chat/protocols/http.ts +118 -0
  98. package/lib/chat/protocols/shell.ts +101 -0
  99. package/lib/chat/proxy.ts +51 -0
  100. package/lib/chat/session-store.ts +272 -0
  101. package/lib/chat/telegram-bridge.ts +276 -0
  102. package/lib/chat/temper.ts +281 -0
  103. package/lib/chat/tool-dispatcher.ts +190 -0
  104. package/lib/chat/types.ts +50 -0
  105. package/lib/chat-standalone.ts +286 -0
  106. package/lib/crypto.ts +1 -1
  107. package/lib/health.ts +131 -0
  108. package/lib/help-docs/00-overview.md +2 -1
  109. package/lib/help-docs/01-settings.md +46 -25
  110. package/lib/help-docs/07-projects.md +1 -1
  111. package/lib/help-docs/10-troubleshooting.md +10 -2
  112. package/lib/help-docs/16-gitlab-autofix.md +114 -0
  113. package/lib/help-docs/17-connectors.md +322 -0
  114. package/lib/help-docs/18-chrome-mcp.md +134 -0
  115. package/lib/help-docs/19-jobs.md +140 -0
  116. package/lib/help-docs/20-mantis-bug-fix.md +115 -0
  117. package/lib/help-docs/CLAUDE.md +10 -0
  118. package/lib/init.ts +137 -50
  119. package/lib/iso-time.ts +30 -0
  120. package/lib/issue-scanner-gitlab.ts +281 -0
  121. package/lib/jobs/dispatcher.ts +217 -0
  122. package/lib/jobs/scheduler.ts +334 -0
  123. package/lib/jobs/store.ts +319 -0
  124. package/lib/jobs/types.ts +117 -0
  125. package/lib/pipeline-scheduler.ts +1 -6
  126. package/lib/pipeline.ts +790 -10
  127. package/lib/plugins/registry.ts +133 -8
  128. package/lib/plugins/templates.ts +83 -0
  129. package/lib/plugins/types.ts +140 -1
  130. package/lib/session-watcher.ts +36 -10
  131. package/lib/settings.ts +65 -33
  132. package/lib/skills.ts +3 -1
  133. package/lib/task-manager.ts +50 -22
  134. package/lib/telegram-bot.ts +71 -0
  135. package/lib/terminal-standalone.ts +58 -36
  136. package/lib/workspace/orchestrator.ts +1 -0
  137. package/middleware.ts +10 -0
  138. package/package.json +3 -2
  139. package/scripts/bench/README.md +1 -1
  140. package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
  141. package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
  142. package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
  143. package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
  144. package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
  145. package/src/core/db/database.ts +21 -12
@@ -0,0 +1,471 @@
1
+ # Forge change request — declarative connector extraction
2
+
3
+ > **Read this first**: full design rationale lives in
4
+ > [`Connector-DeclarativeExtract-Spec.md`](./Connector-DeclarativeExtract-Spec.md).
5
+ > This file is the **change list** for the Forge code + a ready-to-commit
6
+ > `mantis.yaml` so you can ship without further design discussion.
7
+
8
+ ## TL;DR
9
+
10
+ Today: adding a connector means coordinated changes in Forge (manifest) **and** the browser extension (handler code).
11
+ Goal: adding a connector should be a **Forge-only YAML change**. Extension is now a generic runner; it has no Mantis/GitLab-specific code.
12
+
13
+ Status:
14
+ - Extension side is done. `src/lib/connectors/runner.ts` is a generic executor; `mantis.ts` / `registry.ts` / `types.ts` are deleted. The extension can run ANY connector whose manifest contains the new fields.
15
+ - **Forge side is what this doc is about.** Until Forge ships the new fields, the LLM can't see any connector tools (the extension's gate filters out tools without `script`).
16
+
17
+ ## What Forge needs to change
18
+
19
+ ### 1. Plugin manifest parser — accept four new fields
20
+
21
+ Allowed YAML at the plugin level:
22
+
23
+ | Field | Required | Notes |
24
+ |---|---|---|
25
+ | `host_match` | yes (for browser-side mode) | Chrome match pattern. `{settings.*}` template variables allowed. Used by extension to find or open an authenticated tab. |
26
+ | `login_redirect` | optional | Substring. If the tab URL contains this after navigation, the runner returns `loginRequired: true`. |
27
+
28
+ Allowed at the **tool** level (under `tools.<name>`):
29
+
30
+ | Field | Required | Notes |
31
+ |---|---|---|
32
+ | `page.url` | yes (executable tool) | URL the runner navigates to before running the script. `{settings.*}` AND `{args.*}` template variables allowed. |
33
+ | `page.on_target` | optional | Substring. If current tab URL already contains this, skip the navigation step. |
34
+ | `script` | yes (executable tool) | Function body. Receives `args` (LLM's parsed parameters), returns JSON-serializable. Runs in the user's tab. |
35
+
36
+ Tools missing `page` or `script` are still valid manifests — they just won't be callable by the LLM. (Useful for tools that you want to surface in the Connectors UI but haven't yet implemented.)
37
+
38
+ ### 2. `GET /api/connectors[?id=<id>]` response — include the new fields
39
+
40
+ The response body must include the new fields on every connector and every tool. Settings-template substitution happens server-side; args-template substitution happens client-side.
41
+
42
+ ```jsonc
43
+ // GET /api/connectors →
44
+ {
45
+ "connectors": [
46
+ {
47
+ "plugin_id": "mantis",
48
+ "name": "MantisBT",
49
+ "icon": "🐞",
50
+ "version": "0.2.0",
51
+ "mode": "browser-side",
52
+ "installed": true,
53
+
54
+ // NEW — expanded from settings.base_url on the server side
55
+ "host_match": "https://mantis.acme.com/*",
56
+ "login_redirect": "/login_page.php",
57
+
58
+ "entries": [
59
+ {
60
+ "id": "mantis",
61
+ "tools": {
62
+ "list_my_bugs": {
63
+ "description": "...",
64
+ "parameters": {...},
65
+ // NEW — also expanded server-side
66
+ "page": {
67
+ "url": "https://mantis.acme.com/view_all_bug_page.php",
68
+ "on_target": "/view_all_bug_page.php"
69
+ },
70
+ // NEW — verbatim string (no template substitution; runs as JS)
71
+ "script": "const list = document.querySelector('#bug_list');\n..."
72
+ }
73
+ },
74
+ "settings": {...}
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### 3. Template substitution
83
+
84
+ Forge expands at API response time:
85
+ - `{base_url}` → `settings.base_url`
86
+ - `{settings.<name>}` → `settings.<name>`
87
+ - Strip trailing slashes from `base_url` before substituting (so `{base_url}/foo` doesn't double-slash)
88
+
89
+ Forge does **not** expand `{args.*}`. The extension does that at tool-call time. (It also URL-encodes `{args.*}` substitutions inside URLs to be safe.) Forge can ship `{args.bug_id}` literally inside `page.url`.
90
+
91
+ If a tool's `page.url` contains an unexpanded `{settings.foo}` because the user hasn't saved that setting yet, return the tool with `page.url` unchanged (literal `{settings.foo}`). The extension will surface a clear error when called.
92
+
93
+ ### 4. `installed=false` connectors
94
+
95
+ For uninstalled connectors, the user hasn't saved `base_url` etc. Return `page.url` / `host_match` with `{settings.*}` left literal so the Connectors marketplace UI can still preview the manifest.
96
+
97
+ ### 5. Plugin id, version, mode
98
+
99
+ Bump version on every manifest content change. The extension's Connectors tab uses `version` to flag stale cached manifests on refresh. Mode stays `browser-side` for these.
100
+
101
+ ## Acceptance criteria
102
+
103
+ ✅ Adding `lib/builtin-plugins/foo.yaml` with `host_match` + `tools.bar.page` + `tools.bar.script` makes `foo.bar` callable by the LLM after extension refresh, with no extension code changes.
104
+
105
+ ✅ Bumping a `script` body in a YAML and restarting Forge propagates to all extension users on next Connectors tab refresh — no extension release.
106
+
107
+ ✅ Old manifests (no `page` / `script` / `host_match`) parse without error; tools just don't show up as callable.
108
+
109
+ ✅ `mantis.list_my_bugs` works end-to-end:
110
+ 1. User asks "list my Mantis bugs" in chat
111
+ 2. LLM calls `mantis.list_my_bugs`
112
+ 3. Extension runner: finds existing Mantis tab OR opens background tab on `host_match`
113
+ 4. Runner navigates to `page.url`
114
+ 5. Runner executes the `script` body in MAIN world via `chrome.scripting.executeScript`
115
+ 6. LLM gets `{bugs: [...]}` back, summarizes for the user
116
+
117
+ ## Full `mantis.yaml` — ready to commit
118
+
119
+ Drop this in `lib/builtin-plugins/mantis.yaml`. It contains the same five tools as before, with the extension's old `extractBugList` / `extractBugDetail` / `extractProjects` / `addBugnoteInPage` bodies inlined as `script`. Tested against Mantis 1.x classic (Forge self-hosted theme).
120
+
121
+ ```yaml
122
+ id: mantis
123
+ name: MantisBT
124
+ icon: "🐞"
125
+ version: "0.2.0"
126
+ author: forge
127
+ category: connector
128
+ mode: browser-side
129
+ description: |
130
+ Read + comment on MantisBT bugs from the user's logged-in browser session.
131
+ Reuses the existing PHPSESSID cookie — no API token required.
132
+
133
+ All tools run as DOM extraction scripts inside the user's Mantis tab,
134
+ shipped by Forge in this manifest. The extension is generic.
135
+
136
+ settings:
137
+ base_url:
138
+ type: string
139
+ label: Mantis base URL
140
+ description: "e.g. https://mantis.mycompany.com (no trailing slash)"
141
+ required: true
142
+ default_project:
143
+ type: string
144
+ label: Default project name
145
+ description: "Optional — used when no project specified"
146
+
147
+ host_match: "{base_url}/*"
148
+ login_redirect: "/login_page.php"
149
+
150
+ tools:
151
+ list_my_bugs:
152
+ description: |
153
+ List bugs assigned to the current user. Returns id, summary, status,
154
+ severity, priority, project, last_updated.
155
+ parameters:
156
+ status:
157
+ type: select
158
+ options: ["open", "feedback", "acknowledged", "confirmed", "assigned", "resolved", "closed", "all"]
159
+ default: "open"
160
+ limit:
161
+ type: number
162
+ default: 50
163
+ page:
164
+ url: "{base_url}/view_all_bug_page.php"
165
+ on_target: "/view_all_bug_page.php"
166
+ script: |
167
+ const list = document.querySelector('#bug_list');
168
+ if (!list) {
169
+ return { bugs: [], total: 0, _error: '#bug_list not found on ' + location.pathname };
170
+ }
171
+ const allRows = Array.from(list.querySelectorAll('tr'));
172
+ const headerRow = allRows.find(r => r.className === 'row-category');
173
+ const dataRows = allRows.filter(r => r.querySelector('input[name*="bug"]'));
174
+ const headerIdx = {};
175
+ if (headerRow) {
176
+ Array.from(headerRow.querySelectorAll('td, th')).forEach((c, i) => {
177
+ const name = (c.textContent || '').trim().toLowerCase();
178
+ if (name) headerIdx[name] = i;
179
+ });
180
+ }
181
+ const col = (cells, header) => {
182
+ const idx = headerIdx[header.toLowerCase()];
183
+ if (idx === undefined) return '';
184
+ return ((cells[idx] && cells[idx].innerText) || '').trim();
185
+ };
186
+ const STATUS_RE = /^(new|feedback|acknowledged|confirmed|assigned|open|resolved|closed|reopened)$/i;
187
+ const FILTERS = {
188
+ open: /^(new|feedback|acknowledged|confirmed|assigned|open|reopened)$/i,
189
+ closed: /^(resolved|closed)$/i,
190
+ all: /./,
191
+ };
192
+ const all = dataRows.map(r => {
193
+ const cells = Array.from(r.querySelectorAll('td'));
194
+ const checkbox = r.querySelector('input[type="checkbox"][name*="bug"]');
195
+ const link = r.querySelector('a[href*="bug_view_page.php?bug_id="]');
196
+ const id = checkbox && checkbox.value
197
+ ? Number(checkbox.value)
198
+ : Number((link && link.href.match(/bug_id=(\d+)/) || [])[1] || 0);
199
+ if (!id) return null;
200
+ // Forge quirk: "Status" column shows handler; real status is in "Resolution"
201
+ let status = col(cells, 'Status');
202
+ if (!STATUS_RE.test(status)) {
203
+ const resolution = col(cells, 'Resolution');
204
+ if (STATUS_RE.test(resolution)) status = resolution;
205
+ }
206
+ return {
207
+ id,
208
+ summary: (link && link.getAttribute('title') && link.getAttribute('title').trim()) || col(cells, 'Summary'),
209
+ status,
210
+ priority: col(cells, 'Priority'),
211
+ severity: col(cells, 'Severity') || col(cells, 'S'),
212
+ keywords: col(cells, 'Keyword') || col(cells, 'Category'),
213
+ reporter: col(cells, 'Reporter'),
214
+ assignee: (col(cells, 'Status') || col(cells, 'Handler') || col(cells, 'Assigned To')).replace(/^\(|\)$/g, '').trim(),
215
+ source: col(cells, 'Source'),
216
+ product_version: col(cells, 'Reported Version') || col(cells, 'Product Version'),
217
+ fix_schedule: col(cells, 'Fix Schedule'),
218
+ last_updated: col(cells, 'Updated') || col(cells, 'Last Updated'),
219
+ created_at: col(cells, 'Date Submitted') || col(cells, 'Date'),
220
+ url: (link && link.href) || '',
221
+ };
222
+ }).filter(b => b !== null);
223
+ const key = (args.status || 'all').toLowerCase();
224
+ const filter = FILTERS[key] || FILTERS.all;
225
+ const filtered = all.filter(b => filter.test(b.status));
226
+ return {
227
+ bugs: filtered.slice(0, args.limit || 50),
228
+ total: filtered.length,
229
+ rawCount: all.length,
230
+ _page: location.href,
231
+ };
232
+
233
+ get_bug:
234
+ description: |
235
+ Get full details of a single bug — description, all notes, history.
236
+ parameters:
237
+ bug_id:
238
+ type: number
239
+ required: true
240
+ page:
241
+ url: "{base_url}/bug_view_page.php?bug_id={args.bug_id}"
242
+ on_target: "/bug_view_page.php"
243
+ script: |
244
+ const detailsTitle = Array.from(document.querySelectorAll('.form-title'))
245
+ .find(e => e.textContent && e.textContent.trim().startsWith('Viewing Bug'));
246
+ const detailsTable = detailsTitle ? detailsTitle.closest('table') : null;
247
+ if (!detailsTable) return { _error: 'bug detail table not found on ' + location.pathname };
248
+ const rows = Array.from(detailsTable.querySelectorAll('tr'));
249
+ const fields = {};
250
+ if (rows[1] && rows[2]) {
251
+ const labelCells = Array.from(rows[1].querySelectorAll('td'));
252
+ const valueCells = Array.from(rows[2].querySelectorAll('td'));
253
+ labelCells.forEach((l, i) => {
254
+ const label = l.innerText.trim();
255
+ const val = valueCells[i] ? valueCells[i].innerText.trim() : '';
256
+ if (label && !fields[label]) fields[label] = val;
257
+ });
258
+ }
259
+ Array.from(document.querySelectorAll('td.category')).forEach(label => {
260
+ const labelText = (label.textContent || '').trim();
261
+ if (labelText.includes('\n') && labelText.length > 100) return;
262
+ if (labelText.startsWith('Select File')) return;
263
+ if (labelText === 'Bugnote') return;
264
+ const valueEl = label.nextElementSibling;
265
+ if (!valueEl || valueEl.tagName !== 'TD' || valueEl.classList.contains('category')) return;
266
+ const val = valueEl.innerText.trim();
267
+ if (!fields[labelText]) fields[labelText] = val;
268
+ });
269
+ const notesTitle = Array.from(document.querySelectorAll('.form-title'))
270
+ .find(e => e.textContent && e.textContent.trim().startsWith('Bug Notes'));
271
+ const notesTable = notesTitle && notesTitle.closest('table');
272
+ const notes = Array.from((notesTable && notesTable.querySelectorAll('tr.bugnote')) || []).map((r, i) => {
273
+ const cells = r.querySelectorAll('td');
274
+ const header = (cells[0] && cells[0].innerText.trim()) || '';
275
+ const body = (cells[1] && cells[1].innerText.trim()) || '';
276
+ const m = header.match(/^(.*?)\n(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
277
+ return {
278
+ idx: i,
279
+ author: (m && m[1]) ? m[1].trim() : (header.split('\n')[0] || '').trim(),
280
+ date: (m && m[2]) || '',
281
+ body,
282
+ };
283
+ });
284
+ const historyTitle = Array.from(document.querySelectorAll('.form-title'))
285
+ .find(e => e.textContent && e.textContent.trim().startsWith('Bug History'));
286
+ const historyTable = historyTitle && historyTitle.closest('table');
287
+ const history = Array.from((historyTable && historyTable.querySelectorAll('tr')) || [])
288
+ .filter(r => r.querySelectorAll('td').length === 4)
289
+ .slice(1)
290
+ .map(r => {
291
+ const c = Array.from(r.querySelectorAll('td')).map(x => x.innerText.trim());
292
+ return { date: c[0], user: c[1], field: c[2], change: c[3] };
293
+ });
294
+ const id = Number(fields.ID || args.bug_id);
295
+ return {
296
+ id,
297
+ summary: fields.Summary || '',
298
+ description: fields.Description || '',
299
+ additional_information: fields['Additional Information'] || '',
300
+ status: fields.Status || '',
301
+ resolution: fields.Resolution || '',
302
+ priority: fields.Priority || '',
303
+ severity: fields.Severity || '',
304
+ reproducibility: fields.Reproducibility || '',
305
+ category: fields.Category || '',
306
+ keywords: fields.Keyword || '',
307
+ reporter: fields.Reporter || '',
308
+ assignee: fields['Assigned To'] || '',
309
+ qa_assignee: fields['QA Assignee'] || '',
310
+ view_status: fields['View Status'] || '',
311
+ eta: fields.ETA || '',
312
+ date_submitted: fields['Date Submitted'] || '',
313
+ last_update: fields['Last Update'] || '',
314
+ reported_version: fields['Reported Version'] || '',
315
+ fix_schedule: fields['Fix Schedule'] || '',
316
+ source: fields.Source || '',
317
+ tag: fields.Tag || '',
318
+ notes,
319
+ history,
320
+ url: location.href,
321
+ };
322
+
323
+ search_bugs:
324
+ description: |
325
+ Search bugs by text query. Mantis 1.x classic does this via
326
+ view_all_set.php which sets a temporary filter and redirects to
327
+ view_all_bug_page.php with results.
328
+ parameters:
329
+ query:
330
+ type: string
331
+ required: true
332
+ status:
333
+ type: string
334
+ description: "Optional Mantis status keyword: new|feedback|acknowledged|confirmed|assigned|resolved|closed"
335
+ limit:
336
+ type: number
337
+ default: 25
338
+ page:
339
+ # No on_target — we always want to re-set the filter on each call.
340
+ # {args.query} URL-encoded by the extension at call time.
341
+ url: "{base_url}/view_all_set.php?type=1&temporary=y&search={args.query}"
342
+ script: |
343
+ // After view_all_set.php's filter is set, Mantis redirects to
344
+ // view_all_bug_page.php. The script body is identical to list_my_bugs
345
+ // but with `status` forced to 'all' so the in-page filter governs.
346
+ const list = document.querySelector('#bug_list');
347
+ if (!list) return { bugs: [], total: 0, _error: '#bug_list not found' };
348
+ const allRows = Array.from(list.querySelectorAll('tr'));
349
+ const headerRow = allRows.find(r => r.className === 'row-category');
350
+ const dataRows = allRows.filter(r => r.querySelector('input[name*="bug"]'));
351
+ const headerIdx = {};
352
+ if (headerRow) {
353
+ Array.from(headerRow.querySelectorAll('td, th')).forEach((c, i) => {
354
+ const name = (c.textContent || '').trim().toLowerCase();
355
+ if (name) headerIdx[name] = i;
356
+ });
357
+ }
358
+ const col = (cells, header) => {
359
+ const idx = headerIdx[header.toLowerCase()];
360
+ if (idx === undefined) return '';
361
+ return ((cells[idx] && cells[idx].innerText) || '').trim();
362
+ };
363
+ const STATUS_RE = /^(new|feedback|acknowledged|confirmed|assigned|open|resolved|closed|reopened)$/i;
364
+ const all = dataRows.map(r => {
365
+ const cells = Array.from(r.querySelectorAll('td'));
366
+ const checkbox = r.querySelector('input[type="checkbox"][name*="bug"]');
367
+ const link = r.querySelector('a[href*="bug_view_page.php?bug_id="]');
368
+ const id = checkbox && checkbox.value
369
+ ? Number(checkbox.value)
370
+ : Number((link && link.href.match(/bug_id=(\d+)/) || [])[1] || 0);
371
+ if (!id) return null;
372
+ let status = col(cells, 'Status');
373
+ if (!STATUS_RE.test(status)) {
374
+ const resolution = col(cells, 'Resolution');
375
+ if (STATUS_RE.test(resolution)) status = resolution;
376
+ }
377
+ return {
378
+ id,
379
+ summary: (link && link.getAttribute('title') && link.getAttribute('title').trim()) || col(cells, 'Summary'),
380
+ status,
381
+ severity: col(cells, 'Severity') || col(cells, 'S'),
382
+ project: col(cells, 'Project') || col(cells, 'Category'),
383
+ url: (link && link.href) || '',
384
+ };
385
+ }).filter(b => b !== null);
386
+ const limit = args.limit || 25;
387
+ return {
388
+ query: args.query,
389
+ bugs: all.slice(0, limit),
390
+ total: all.length,
391
+ };
392
+
393
+ list_projects:
394
+ description: List projects visible to the current user (from the project_id dropdown on the list page).
395
+ page:
396
+ url: "{base_url}/view_all_bug_page.php"
397
+ on_target: "/view_all_bug_page.php"
398
+ script: |
399
+ const sel = document.querySelector('select[name="project_id"]');
400
+ if (!sel) return { _error: 'project_id select not found on page' };
401
+ const projects = Array.from(sel.options)
402
+ .map(o => ({
403
+ id: Number(o.value),
404
+ name: (o.text || '').trim(),
405
+ depth: ((o.text || '').match(/^[\s ]+/) || ['']).length,
406
+ }))
407
+ .filter(p => p.id > 0);
408
+ return { projects, total: projects.length };
409
+
410
+ add_comment:
411
+ description: |
412
+ Add a note (comment) to a bug. Posts to bugnote_add.php with the
413
+ session cookie. Destructive — extension prompts the user first.
414
+ destructive: true
415
+ parameters:
416
+ bug_id:
417
+ type: number
418
+ required: true
419
+ text:
420
+ type: string
421
+ required: true
422
+ page:
423
+ url: "{base_url}/bug_view_page.php?bug_id={args.bug_id}"
424
+ on_target: "/bug_view_page.php"
425
+ script: |
426
+ const body = new URLSearchParams({
427
+ bug_id: String(args.bug_id),
428
+ bugnote_text: args.text,
429
+ });
430
+ try {
431
+ const r = await fetch('bugnote_add.php', {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
434
+ body: body.toString(),
435
+ credentials: 'include',
436
+ redirect: 'follow',
437
+ });
438
+ return {
439
+ ok: r.ok,
440
+ status: r.status,
441
+ finalUrl: r.url,
442
+ success: r.ok && r.url.indexOf('bug_id=' + args.bug_id) >= 0,
443
+ };
444
+ } catch (e) {
445
+ return { ok: false, error: e.message };
446
+ }
447
+ ```
448
+
449
+ ## Notes on the YAML
450
+
451
+ - **Block scalar `|`** preserves newlines verbatim. The `script` content is plain JS, no TypeScript syntax, no imports — runs as the body of `new Function('args', script)`.
452
+ - **No closures**: scripts can't reference variables outside `args` and the page's global Web APIs (`document`, `fetch`, etc.).
453
+ - **`async/await` allowed**: the runner uses `new Function('args', script)` which returns a value; if that value is a Promise, the runner awaits it. (See `runner.ts` — the wrapper handles both sync and async return values.) The `add_comment` script uses `await fetch(...)` — works fine.
454
+ - **`{args.foo}` substitution and URL encoding**: the extension encodes args values before substituting into URLs. So `{args.query}` with `q="hello world"` becomes `search=hello%20world`, not `search=hello world`.
455
+ - **Forge-specific column quirks** (Status vs Resolution fallback) are baked into the `list_my_bugs` script. If you have a non-Forge Mantis instance, you may need a separate `mantis-vanilla.yaml`.
456
+
457
+ ## Open questions / nice-to-haves (not blockers)
458
+
459
+ - **Probe script delivery** — `mantis.probe.js` stays as dev-time tool. If we want auto-recovery from selector drift, expose `GET /api/connectors/<id>/probe` later.
460
+ - **Server-side `script` linting** — Forge could `new Function(scriptStr)` at load time to catch parse errors before the manifest ever ships to the extension. Optional.
461
+ - **Schema versioning** — when we change the manifest schema again in the future, bump a top-level `schema_version` and let the extension reject incompatible older shapes.
462
+
463
+ ## What happens when Forge ships this
464
+
465
+ 1. User refreshes the Connectors tab in the extension.
466
+ 2. Extension fetches `/api/connectors`, sees `script` + `page` populated on each Mantis tool.
467
+ 3. `buildConnectorTools()` un-filters those tools (they now satisfy the `tool.script` gate).
468
+ 4. On the next chat turn, the LLM's tool list includes `mantis.list_my_bugs`, `mantis.get_bug`, `mantis.search_bugs`, `mantis.list_projects`, `mantis.add_comment`.
469
+ 5. User asks "list my Mantis bugs" → LLM calls `mantis.list_my_bugs` → runner opens background Mantis tab (or reuses existing) → executes the YAML's `script` body in MAIN world → returns JSON to LLM → LLM summarizes.
470
+
471
+ No extension code changes required at any point in this flow.