@aion0/forge 0.10.23 → 0.10.26
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 +13 -20
- package/app/api/connectors/[id]/test/route.ts +8 -274
- package/app/api/login-status/[id]/check/route.ts +22 -0
- package/app/api/login-status/route.ts +36 -0
- package/components/Dashboard.tsx +26 -0
- package/components/LoginStatusPanel.tsx +223 -0
- package/components/SettingsModal.tsx +0 -6
- package/docs/blackduck-openapi.yaml +9626 -0
- package/docs/tp-automation-api-v2.md +482 -0
- package/lib/auth/login-status.ts +291 -0
- package/lib/chat/protocols/http.ts +102 -8
- package/lib/chat/tool-dispatcher.ts +1 -1
- package/lib/connectors/test-runner.ts +239 -0
- package/lib/connectors/types.ts +58 -1
- package/lib/help-docs/18-chrome-mcp.md +22 -7
- package/lib/pipeline.ts +2 -2
- package/lib/settings.ts +17 -6
- package/lib/task-manager.ts +4 -4
- package/lib/telegram-bot.ts +3 -3
- package/lib/watch/watch-runner.ts +20 -2
- package/package.json +2 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector test runner — extracted from the /api/connectors/:id/test
|
|
3
|
+
* route so both that HTTP endpoint AND server-side callers (e.g. the
|
|
4
|
+
* Login Status panel) can run probes without a self-fetch dance.
|
|
5
|
+
*
|
|
6
|
+
* Self-fetching the route from inside the server hits the auth
|
|
7
|
+
* middleware (no session cookie → 401), making every probe look
|
|
8
|
+
* "unauthorized" regardless of the real connector state. Direct
|
|
9
|
+
* function calls bypass that.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getConnector,
|
|
14
|
+
getConnectorEntries,
|
|
15
|
+
getInstalledConnector,
|
|
16
|
+
} from './registry';
|
|
17
|
+
import { expandSettingsTokens, expandAllTokens } from '../plugins/templates';
|
|
18
|
+
import { bridgeRpc } from '../chat/bridge-client';
|
|
19
|
+
import { applyAuth } from '../chat/protocols/http';
|
|
20
|
+
import type { ConnectorDefinition, ConnectorTest, HttpRequestSpec } from './types';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
23
|
+
const MAX_BODY_PREVIEW = 1024;
|
|
24
|
+
|
|
25
|
+
export interface TestResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
status?: number;
|
|
28
|
+
message?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
duration_ms?: number;
|
|
31
|
+
body_preview?: string;
|
|
32
|
+
/** Final landed URL for browser probes — helps diagnose redirects. */
|
|
33
|
+
url?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expandString(s: string, settings: Record<string, unknown>): string {
|
|
37
|
+
return expandAllTokens(s, settings as Record<string, any>, {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildUrl(spec: HttpRequestSpec, settings: Record<string, unknown>): string {
|
|
41
|
+
let url = expandString(spec.url, settings);
|
|
42
|
+
if (spec.query) {
|
|
43
|
+
const u = new URL(url);
|
|
44
|
+
for (const [k, raw] of Object.entries(spec.query)) {
|
|
45
|
+
u.searchParams.set(k, expandString(String(raw), settings));
|
|
46
|
+
}
|
|
47
|
+
url = u.toString();
|
|
48
|
+
}
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildHeaders(spec: HttpRequestSpec, settings: Record<string, unknown>): Headers {
|
|
53
|
+
const h = new Headers();
|
|
54
|
+
if (spec.headers) {
|
|
55
|
+
for (const [k, raw] of Object.entries(spec.headers)) {
|
|
56
|
+
h.set(k, expandString(String(raw), settings));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return h;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildBody(
|
|
63
|
+
spec: HttpRequestSpec,
|
|
64
|
+
settings: Record<string, unknown>,
|
|
65
|
+
): { body?: string; contentType?: string } {
|
|
66
|
+
if (spec.body == null) return {};
|
|
67
|
+
if (typeof spec.body === 'string') {
|
|
68
|
+
return { body: expandString(spec.body, settings) };
|
|
69
|
+
}
|
|
70
|
+
const out: Record<string, unknown> = {};
|
|
71
|
+
for (const [k, v] of Object.entries(spec.body)) {
|
|
72
|
+
out[k] = typeof v === 'string' ? expandString(v, settings) : v;
|
|
73
|
+
}
|
|
74
|
+
return { body: JSON.stringify(out), contentType: 'application/json' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderTemplate(template: string, body: unknown): string {
|
|
78
|
+
return template.replace(/\{\{([^{}]+)\}\}/g, (_match, expr) => {
|
|
79
|
+
const path = String(expr).trim().split('.').filter(Boolean);
|
|
80
|
+
let cur: any = body;
|
|
81
|
+
for (const p of path) {
|
|
82
|
+
if (cur && typeof cur === 'object' && p in cur) cur = cur[p];
|
|
83
|
+
else return '?';
|
|
84
|
+
}
|
|
85
|
+
if (cur == null) return '?';
|
|
86
|
+
return typeof cur === 'string' ? cur : JSON.stringify(cur);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runHttpProbe(
|
|
91
|
+
test: ConnectorTest,
|
|
92
|
+
settings: Record<string, unknown>,
|
|
93
|
+
def: ConnectorDefinition,
|
|
94
|
+
): Promise<TestResult> {
|
|
95
|
+
const spec = test.request;
|
|
96
|
+
if (!spec?.url) return { ok: false, error: 'test.request.url is required for http probe' };
|
|
97
|
+
|
|
98
|
+
// Multi-instance overlay: first instance only (matches tool-dispatcher).
|
|
99
|
+
let effectiveSettings = settings as Record<string, any>;
|
|
100
|
+
let instances = effectiveSettings?.instances;
|
|
101
|
+
if (typeof instances === 'string') {
|
|
102
|
+
try { instances = JSON.parse(instances); } catch { instances = null; }
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
Array.isArray(instances) &&
|
|
106
|
+
instances.length > 0 &&
|
|
107
|
+
instances.every((i: any) => i && typeof i === 'object' && typeof i.name === 'string')
|
|
108
|
+
) {
|
|
109
|
+
effectiveSettings = { ...effectiveSettings, ...instances[0] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const method = (spec.method || 'GET').toUpperCase();
|
|
113
|
+
let url = buildUrl(spec, effectiveSettings);
|
|
114
|
+
const headers = buildHeaders(spec, effectiveSettings);
|
|
115
|
+
const { body, contentType } = buildBody(spec, effectiveSettings);
|
|
116
|
+
if (body != null && contentType && !headers.has('content-type')) {
|
|
117
|
+
headers.set('content-type', contentType);
|
|
118
|
+
}
|
|
119
|
+
url = await applyAuth(url, headers, def.auth, effectiveSettings);
|
|
120
|
+
|
|
121
|
+
const timeoutMs = test.timeout_ms || DEFAULT_TIMEOUT_MS;
|
|
122
|
+
const okStatus = test.ok_status?.length ? test.ok_status : [200];
|
|
123
|
+
|
|
124
|
+
const ctrl = new AbortController();
|
|
125
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
let res: Response;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetch(url, { method, headers, body, signal: ctrl.signal });
|
|
130
|
+
} catch (e) {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
const err = e as Error & { cause?: unknown };
|
|
133
|
+
const cause = err.cause instanceof Error ? `: ${err.cause.message}` : '';
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `request failed: ${err.message}${cause}`,
|
|
137
|
+
duration_ms: Date.now() - t0,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
|
|
142
|
+
const duration = Date.now() - t0;
|
|
143
|
+
const text = await res.text().catch(() => '');
|
|
144
|
+
const preview = text.length > MAX_BODY_PREVIEW ? text.slice(0, MAX_BODY_PREVIEW) + '…' : text;
|
|
145
|
+
|
|
146
|
+
if (!okStatus.includes(res.status)) {
|
|
147
|
+
let errMsg = `HTTP ${res.status} ${res.statusText}`;
|
|
148
|
+
try {
|
|
149
|
+
const j = JSON.parse(text);
|
|
150
|
+
if (typeof j?.error === 'string') errMsg += `: ${j.error}`;
|
|
151
|
+
else if (typeof j?.message === 'string') errMsg += `: ${j.message}`;
|
|
152
|
+
else if (typeof j?.error_description === 'string') errMsg += `: ${j.error_description}`;
|
|
153
|
+
} catch {}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
status: res.status,
|
|
157
|
+
error: errMsg,
|
|
158
|
+
duration_ms: duration,
|
|
159
|
+
body_preview: preview,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let parsedBody: unknown = null;
|
|
164
|
+
try { parsedBody = JSON.parse(text); } catch {}
|
|
165
|
+
const message = test.ok_template
|
|
166
|
+
? renderTemplate(test.ok_template, parsedBody)
|
|
167
|
+
: `OK (HTTP ${res.status})`;
|
|
168
|
+
return { ok: true, status: res.status, message, duration_ms: duration };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function runBrowserProbe(
|
|
172
|
+
def: ConnectorDefinition,
|
|
173
|
+
settings: Record<string, unknown>,
|
|
174
|
+
): Promise<TestResult> {
|
|
175
|
+
const entries = getConnectorEntries(def);
|
|
176
|
+
const entry = entries[0];
|
|
177
|
+
const hostMatch = def.host_match || entry?.host_match;
|
|
178
|
+
const loginRedirect = def.login_redirect || entry?.login_redirect;
|
|
179
|
+
if (!hostMatch) {
|
|
180
|
+
return { ok: false, error: 'browser probe requires host_match on the manifest' };
|
|
181
|
+
}
|
|
182
|
+
const expandedHost = expandSettingsTokens(hostMatch, settings as any);
|
|
183
|
+
const expandedLoginRedirect = loginRedirect
|
|
184
|
+
? expandSettingsTokens(loginRedirect, settings as any)
|
|
185
|
+
: undefined;
|
|
186
|
+
|
|
187
|
+
const t0 = Date.now();
|
|
188
|
+
let value: unknown;
|
|
189
|
+
try {
|
|
190
|
+
value = await bridgeRpc('connector.probe', {
|
|
191
|
+
pluginId: def.id,
|
|
192
|
+
host_match: expandedHost,
|
|
193
|
+
login_redirect: expandedLoginRedirect,
|
|
194
|
+
runner: def.runner || entry?.runner || 'main',
|
|
195
|
+
timeout_ms: def.test?.timeout_ms || 30_000,
|
|
196
|
+
});
|
|
197
|
+
} catch (e) {
|
|
198
|
+
const raw = (e as Error).message || String(e);
|
|
199
|
+
let friendly = raw;
|
|
200
|
+
if (raw.includes('unknown method: connector.probe')) {
|
|
201
|
+
friendly =
|
|
202
|
+
'Your Forge browser extension is out of date — it doesn\'t know how to run browser probes yet. ' +
|
|
203
|
+
'Rebuild the extension (pnpm ext in forge-browser-extension), then chrome://extensions → Reload, and try Test again.';
|
|
204
|
+
} else if (raw.includes('bridge') && raw.includes('unreachable')) {
|
|
205
|
+
friendly =
|
|
206
|
+
'Forge browser bridge is unreachable on port 8407. Restart Forge (forge server restart) or check that the browser-bridge standalone is running.';
|
|
207
|
+
} else if (raw.includes('no paired extensions') || raw.includes('no connected')) {
|
|
208
|
+
friendly =
|
|
209
|
+
'Forge browser extension not connected. Install the extension from forge-browser-extension/dist, pin it, and sign in with your Forge URL + admin password.';
|
|
210
|
+
}
|
|
211
|
+
return { ok: false, error: friendly, duration_ms: Date.now() - t0 };
|
|
212
|
+
}
|
|
213
|
+
const r = (value || {}) as { ok?: boolean; url?: string; error?: string };
|
|
214
|
+
return {
|
|
215
|
+
ok: !!r.ok,
|
|
216
|
+
message: r.ok ? `Session active${r.url ? ` · ${r.url}` : ''}` : undefined,
|
|
217
|
+
error: r.ok ? undefined : (r.error || 'login required'),
|
|
218
|
+
url: r.url,
|
|
219
|
+
duration_ms: Date.now() - t0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Run a connector's `test:` probe. Returns a structured result. The HTTP
|
|
225
|
+
* route + Login Status panel both call this — they share the same
|
|
226
|
+
* semantics for free.
|
|
227
|
+
*/
|
|
228
|
+
export async function runConnectorTest(id: string): Promise<TestResult & { code?: number }> {
|
|
229
|
+
const def = getConnector(id);
|
|
230
|
+
if (!def) return { ok: false, error: 'connector not found', code: 404 };
|
|
231
|
+
if (!def.test) return { ok: false, error: 'connector has no test block', code: 400 };
|
|
232
|
+
const inst = getInstalledConnector(id);
|
|
233
|
+
if (!inst) return { ok: false, error: 'connector not installed', code: 400 };
|
|
234
|
+
|
|
235
|
+
const probe = def.test.probe || 'http';
|
|
236
|
+
return probe === 'browser'
|
|
237
|
+
? await runBrowserProbe(def, inst.config)
|
|
238
|
+
: await runHttpProbe(def.test, inst.config, def);
|
|
239
|
+
}
|
package/lib/connectors/types.ts
CHANGED
|
@@ -159,7 +159,36 @@ export type ConnectorAuth =
|
|
|
159
159
|
| { type: 'basic'; username: string; password: string }
|
|
160
160
|
| { type: 'bearer'; token: string }
|
|
161
161
|
| { type: 'header'; name: string; value: string }
|
|
162
|
-
| { type: 'query'; name: string; value: string }
|
|
162
|
+
| { type: 'query'; name: string; value: string }
|
|
163
|
+
/**
|
|
164
|
+
* Two-step bearer-token exchange, e.g. Black Duck:
|
|
165
|
+
* POST {exchange_url} Authorization: {exchange_auth_header}
|
|
166
|
+
* → { bearerToken, expiresInMilliseconds }
|
|
167
|
+
* ...then every subsequent request gets `Authorization: Bearer <jwt>`.
|
|
168
|
+
* Forge caches the bearer in-memory keyed by (api_token, exchange_url)
|
|
169
|
+
* and refreshes when within 60s of expiry. Tool calls stay fast — only
|
|
170
|
+
* the first call per ~2h pays the exchange round-trip.
|
|
171
|
+
*/
|
|
172
|
+
| {
|
|
173
|
+
type: 'bearer-token-exchange';
|
|
174
|
+
/** Long-lived API token sent to the exchange endpoint. Templated. */
|
|
175
|
+
api_token: string;
|
|
176
|
+
/** Exchange URL. Templated — usually `{base_url}/api/tokens/authenticate`. */
|
|
177
|
+
exchange_url: string;
|
|
178
|
+
/** HTTP method for the exchange. Default POST. */
|
|
179
|
+
exchange_method?: 'POST' | 'GET';
|
|
180
|
+
/**
|
|
181
|
+
* Authorization header VALUE sent to the exchange endpoint.
|
|
182
|
+
* Default `token {api_token}` (Black Duck style). Templated.
|
|
183
|
+
*/
|
|
184
|
+
exchange_auth_header?: string;
|
|
185
|
+
/** Extra headers (Accept etc.) for the exchange. Values templated. */
|
|
186
|
+
exchange_headers?: Record<string, string>;
|
|
187
|
+
/** JSON path to the bearer in the exchange response. Default `bearerToken`. */
|
|
188
|
+
bearer_path?: string;
|
|
189
|
+
/** JSON path to expiry ms in the exchange response. Default `expiresInMilliseconds`. */
|
|
190
|
+
expires_path?: string;
|
|
191
|
+
};
|
|
163
192
|
|
|
164
193
|
/**
|
|
165
194
|
* Server-side HTTP request shape, used by `protocol: http` tools.
|
|
@@ -205,6 +234,21 @@ export interface HttpRequestSpec {
|
|
|
205
234
|
* rather than baking them into the manifest.
|
|
206
235
|
*/
|
|
207
236
|
body_form_inject_from?: string;
|
|
237
|
+
/**
|
|
238
|
+
* Names (case-insensitive) of response headers to surface in the
|
|
239
|
+
* truncated preamble. Default empty. Use when the connector needs
|
|
240
|
+
* a value the upstream returns via headers — typically `set-cookie`
|
|
241
|
+
* for session bootstrap, `location` for create-redirect APIs, or
|
|
242
|
+
* `x-ratelimit-remaining` for backoff logic.
|
|
243
|
+
*
|
|
244
|
+
* Headers appear between the status line and the blank line that
|
|
245
|
+
* separates preamble from body, one per line as `name: value`.
|
|
246
|
+
* Pipeline scripts can grep them with sed/awk before the body parse:
|
|
247
|
+
* JS=$(echo "$RESP" | jq -r '.content' | sed -n 's/^set-cookie: //p' | …)
|
|
248
|
+
*
|
|
249
|
+
* Ignored when `noTruncation: true` (raw-body mode for Jobs scheduler).
|
|
250
|
+
*/
|
|
251
|
+
capture_response_headers?: string[];
|
|
208
252
|
}
|
|
209
253
|
|
|
210
254
|
/**
|
|
@@ -432,6 +476,19 @@ export interface ConnectorDefinition {
|
|
|
432
476
|
*/
|
|
433
477
|
auth?: ConnectorAuth;
|
|
434
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Connector-level HTTP runtime knobs. Apply to every `protocol: http`
|
|
481
|
+
* tool in this connector unless overridden per-tool.
|
|
482
|
+
*
|
|
483
|
+
* `verify_tls: false` disables TLS cert verification — necessary for
|
|
484
|
+
* appliances that ship self-signed certs (FortiNAC, ESXi, internal
|
|
485
|
+
* services). Set this ONLY for hosts you trust by IP; turning it off
|
|
486
|
+
* means an attacker on-path could impersonate the server.
|
|
487
|
+
*/
|
|
488
|
+
http?: {
|
|
489
|
+
verify_tls?: boolean;
|
|
490
|
+
};
|
|
491
|
+
|
|
435
492
|
// ─── 1:N suite — list of sibling entries sharing auth ──
|
|
436
493
|
connectors?: ConnectorEntry[];
|
|
437
494
|
|
|
@@ -25,29 +25,44 @@ chrome-devtools-mcp ──CDP──▶ Chrome (user's real instance)
|
|
|
25
25
|
|
|
26
26
|
### 1. Start Chrome with remote debugging enabled
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
**Easiest path — use the bundled script:**
|
|
29
|
+
```bash
|
|
30
|
+
./scripts/chrome-mcp.sh restart # stop + start with debug port 9222
|
|
31
|
+
./scripts/chrome-mcp.sh status # check whether port is up
|
|
32
|
+
```
|
|
33
|
+
Subcommands: `start`, `stop`, `restart`, `status`. Use `-p PORT` to override.
|
|
34
|
+
|
|
35
|
+
**Manual path** — quit all Chrome windows first (so the new instance
|
|
36
|
+
can take the debug port), then:
|
|
30
37
|
|
|
31
38
|
**macOS:**
|
|
32
39
|
```bash
|
|
33
40
|
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
34
41
|
--remote-debugging-port=9222 \
|
|
35
|
-
--user-data-dir="$HOME
|
|
42
|
+
--user-data-dir="$HOME/.chrome-mcp-profile"
|
|
36
43
|
```
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
**IMPORTANT — non-default profile required.** Chrome refuses to open
|
|
46
|
+
the remote debug port on the default user-data-dir (security guard, so
|
|
47
|
+
localhost code can't hijack your real Chrome with all its logins). The
|
|
48
|
+
error message reads: `DevTools remote debugging requires a non-default
|
|
49
|
+
data directory`.
|
|
50
|
+
|
|
51
|
+
Use a separate path like `$HOME/.chrome-mcp-profile`. **One-time cost:**
|
|
52
|
+
this profile starts empty, so you log in to NAC / Mantis / GitLab once
|
|
53
|
+
inside it. Cookies persist across restarts — wipe with
|
|
54
|
+
`rm -rf "$HOME/.chrome-mcp-profile"` if you want a fresh start.
|
|
40
55
|
|
|
41
56
|
**Linux:**
|
|
42
57
|
```bash
|
|
43
|
-
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.
|
|
58
|
+
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-mcp-profile"
|
|
44
59
|
```
|
|
45
60
|
|
|
46
61
|
**Windows:**
|
|
47
62
|
```powershell
|
|
48
63
|
"C:\Program Files\Google\Chrome\Application\chrome.exe" `
|
|
49
64
|
--remote-debugging-port=9222 `
|
|
50
|
-
--user-data-dir="$env:
|
|
65
|
+
--user-data-dir="$env:USERPROFILE\.chrome-mcp-profile"
|
|
51
66
|
```
|
|
52
67
|
|
|
53
68
|
Verify it worked: open `http://localhost:9222/json/version` in the new
|
package/lib/pipeline.ts
CHANGED
|
@@ -1930,8 +1930,8 @@ async function scheduleReadyNodes(pipeline: Pipeline, workflow: Workflow) {
|
|
|
1930
1930
|
if (skillsAppend) {
|
|
1931
1931
|
taskAppendSystemPromptOverrides.set(task.id, skillsAppend);
|
|
1932
1932
|
}
|
|
1933
|
-
// Pipeline tasks use the same model selection as normal tasks
|
|
1934
|
-
//
|
|
1933
|
+
// Pipeline tasks use the same model selection as normal tasks
|
|
1934
|
+
// (per-task override > agent scene model > agent default).
|
|
1935
1935
|
|
|
1936
1936
|
nodeState.status = 'running';
|
|
1937
1937
|
nodeState.taskId = task.id;
|
package/lib/settings.ts
CHANGED
|
@@ -94,9 +94,6 @@ export interface Settings {
|
|
|
94
94
|
notifyOnFailure: boolean;
|
|
95
95
|
tunnelAutoStart: boolean;
|
|
96
96
|
telegramTunnelPassword: string;
|
|
97
|
-
taskModel: string;
|
|
98
|
-
pipelineModel: string;
|
|
99
|
-
telegramModel: string;
|
|
100
97
|
skipPermissions: boolean;
|
|
101
98
|
manageClaudeConfig: boolean;
|
|
102
99
|
notificationRetentionDays: number;
|
|
@@ -206,9 +203,6 @@ const defaults: Settings = {
|
|
|
206
203
|
notifyOnFailure: true,
|
|
207
204
|
tunnelAutoStart: false,
|
|
208
205
|
telegramTunnelPassword: '',
|
|
209
|
-
taskModel: 'default',
|
|
210
|
-
pipelineModel: 'default',
|
|
211
|
-
telegramModel: 'sonnet',
|
|
212
206
|
skipPermissions: false,
|
|
213
207
|
manageClaudeConfig: true,
|
|
214
208
|
notificationRetentionDays: 30,
|
|
@@ -310,6 +304,23 @@ export function loadSettings(): Settings {
|
|
|
310
304
|
);
|
|
311
305
|
}
|
|
312
306
|
}
|
|
307
|
+
// taskModel / pipelineModel / telegramModel were removed: no UI ever
|
|
308
|
+
// exposed them, and the resolution chain now goes per-task override
|
|
309
|
+
// → agent scene model → agent default. Drop leftover keys; warn when
|
|
310
|
+
// they held a non-default value so the user knows their override
|
|
311
|
+
// stopped applying.
|
|
312
|
+
for (const k of ['taskModel', 'pipelineModel', 'telegramModel'] as const) {
|
|
313
|
+
if (k in parsed) {
|
|
314
|
+
const old = parsed[k];
|
|
315
|
+
delete parsed[k];
|
|
316
|
+
if (old && old !== '' && old !== 'default' && old !== 'sonnet') {
|
|
317
|
+
console.warn(
|
|
318
|
+
`[settings] ${k}="${old}" is no longer read. ` +
|
|
319
|
+
`Set the model on the relevant agent (settings.agents.<id>.models.task) instead.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
313
324
|
// Decrypt top-level secret fields
|
|
314
325
|
for (const field of SECRET_FIELDS) {
|
|
315
326
|
if (parsed[field] && isEncrypted(parsed[field])) {
|
package/lib/task-manager.ts
CHANGED
|
@@ -54,7 +54,7 @@ function db() {
|
|
|
54
54
|
return getDb(getDbPath());
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Per-task model overrides
|
|
57
|
+
// Per-task model overrides — top of the model-resolution chain.
|
|
58
58
|
export const taskModelOverrides = new Map<string, string>();
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -514,12 +514,12 @@ function executeTask(task: Task): Promise<void> {
|
|
|
514
514
|
const agentId = (task as any).agent || settings.defaultAgent || 'claude';
|
|
515
515
|
const adapter = getAgent(agentId);
|
|
516
516
|
|
|
517
|
-
// Model priority: per-task override > agent scene model >
|
|
518
|
-
// "default" means "no override"
|
|
517
|
+
// Model priority: per-task override > agent scene model > 'default'
|
|
518
|
+
// (agent picks its own default). "default" means "no override".
|
|
519
519
|
const agentCfg = settings.agents?.[agentId];
|
|
520
520
|
const agentModel = agentCfg?.models?.task;
|
|
521
521
|
const effectiveAgentModel = agentModel && agentModel !== 'default' ? agentModel : null;
|
|
522
|
-
const model = taskModelOverrides.get(task.id) || effectiveAgentModel ||
|
|
522
|
+
const model = taskModelOverrides.get(task.id) || effectiveAgentModel || 'default';
|
|
523
523
|
const supportsModel = adapter.config.capabilities?.supportsModel;
|
|
524
524
|
const spawnOpts = adapter.buildTaskSpawn({
|
|
525
525
|
projectPath: task.projectPath,
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -736,7 +736,7 @@ async function handlePeek(chatId: number, projectArg?: string, sessionArg?: stri
|
|
|
736
736
|
contextLen += line.length;
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
-
const telegramModel =
|
|
739
|
+
const telegramModel = 'sonnet';
|
|
740
740
|
const summary = contextEntries.length > 3
|
|
741
741
|
? await aiSummarize(contextEntries.join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
742
742
|
: '';
|
|
@@ -1353,7 +1353,7 @@ async function aiSummarize(content: string, instruction: string): Promise<string
|
|
|
1353
1353
|
try {
|
|
1354
1354
|
const settings = loadSettings();
|
|
1355
1355
|
const claudePath = settings.claudePath || process.env.CLAUDE_PATH || 'claude';
|
|
1356
|
-
const model =
|
|
1356
|
+
const model = 'sonnet';
|
|
1357
1357
|
const { execSync } = require('child_process');
|
|
1358
1358
|
const { realpathSync } = require('fs');
|
|
1359
1359
|
|
|
@@ -1509,7 +1509,7 @@ async function handleDocs(chatId: number, input: string) {
|
|
|
1509
1509
|
} catch {}
|
|
1510
1510
|
|
|
1511
1511
|
const recent = entries.slice(-8).join('\n\n');
|
|
1512
|
-
const tModel =
|
|
1512
|
+
const tModel = 'sonnet';
|
|
1513
1513
|
const summary = entries.length > 3
|
|
1514
1514
|
? await aiSummarize(entries.slice(-15).join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
1515
1515
|
: '';
|
|
@@ -89,7 +89,11 @@ export function startWatchRunner(hooks: WatchRunnerHooks = {}): void {
|
|
|
89
89
|
}
|
|
90
90
|
let res;
|
|
91
91
|
try {
|
|
92
|
-
|
|
92
|
+
// noTruncation=true so http-protocol polls skip the "HTTP 200 OK · GET …"
|
|
93
|
+
// preamble — the body comes back as raw JSON so done_path/done_match can
|
|
94
|
+
// see it. (Without this, every http-protocol watch would silently never
|
|
95
|
+
// hit its done condition, e.g. jenkins.get_build never resolving.)
|
|
96
|
+
res = await dispatchTool({ id: `watch-${w.id}-${w.polls}`, name: `${w.connector_id}.${w.poll_tool}`, input: w.poll_args }, { noTruncation: true } as any);
|
|
93
97
|
} catch (e) {
|
|
94
98
|
res = { content: String(e), is_error: true };
|
|
95
99
|
}
|
|
@@ -129,7 +133,21 @@ export function startWatchRunner(hooks: WatchRunnerHooks = {}): void {
|
|
|
129
133
|
if (polls >= w.max_polls || now - w.created_at > w.timeout_sec * 1000) {
|
|
130
134
|
return finish(w, 'timed_out', obj, `${w.label}: not done within ${w.max_polls} polls / ${w.timeout_sec}s — please verify manually.`);
|
|
131
135
|
}
|
|
132
|
-
|
|
136
|
+
// Persist the latest poll result + a tiny preview text on EVERY poll
|
|
137
|
+
// (not just terminal) so the Monitor / DB shows what the watch is
|
|
138
|
+
// actually seeing — crucial for diagnosing "polling forever, no done":
|
|
139
|
+
// usually means the done condition refers to a field the poll's
|
|
140
|
+
// result doesn't actually have.
|
|
141
|
+
const previewKeys = obj && typeof obj === 'object' && !Array.isArray(obj)
|
|
142
|
+
? Object.keys(obj as Record<string, unknown>).slice(0, 8).join(', ')
|
|
143
|
+
: typeof obj;
|
|
144
|
+
updateWatch(w.id, {
|
|
145
|
+
polls,
|
|
146
|
+
err_count: 0,
|
|
147
|
+
next_poll_at: now + w.interval_sec * 1000,
|
|
148
|
+
last_result: obj,
|
|
149
|
+
last_text: `poll ${polls}/${w.max_polls} · keys: ${previewKeys}`,
|
|
150
|
+
}, now);
|
|
133
151
|
emitProgress(w, obj, polls);
|
|
134
152
|
};
|
|
135
153
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aion0/forge",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.26",
|
|
4
4
|
"description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"react-dom": "^19.2.4",
|
|
56
56
|
"react-markdown": "^10.1.0",
|
|
57
57
|
"remark-gfm": "^4.0.1",
|
|
58
|
+
"undici": "^8.3.0",
|
|
58
59
|
"ws": "^8.19.0",
|
|
59
60
|
"yaml": "^2.8.2",
|
|
60
61
|
"zod": "^4.3.6"
|