claude-flow 3.15.0 → 3.16.1
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/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.d.ts +139 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.js +358 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.js +65 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.d.ts +96 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.js +225 -0
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +74 -16
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +16 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +394 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.js +169 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.d.ts +55 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.js +329 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +6 -0
- package/v3/@claude-flow/cli/package.json +2 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http_fetch MCP tool — ADR-164 §5.1.8.
|
|
3
|
+
*
|
|
4
|
+
* The Operations business pod (templates/ops.json) references `http_fetch` as
|
|
5
|
+
* an allowed MCP tool for the synthetic-endpoint availability bench (probe
|
|
6
|
+
* 200/500 from a configured URL, escalate to #ops on 500-rate spikes).
|
|
7
|
+
* Phase 3 left this as a TODO; Phase 4 ships the tool with secure-by-default
|
|
8
|
+
* gating per the §5.1.8 contract: URL allowlist (no file://, ftp://, no
|
|
9
|
+
* RFC-1918 / loopback unless explicitly enabled), header sanitization (no
|
|
10
|
+
* auth pass-through unless explicitly enabled), hard timeout via
|
|
11
|
+
* AbortController, response truncation, default User-Agent.
|
|
12
|
+
*
|
|
13
|
+
* Architectural constraints (load-bearing):
|
|
14
|
+
* - DEFAULT-REFUSES private addresses + loopback (CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 opt-in)
|
|
15
|
+
* - DEFAULT-REFUSES Authorization / Cookie / X-Auth-* (CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 opt-in)
|
|
16
|
+
* - hard timeout 30s default, 60s ceiling
|
|
17
|
+
* - response truncated to 256KB default, 1MB ceiling
|
|
18
|
+
* - no redirects auto-followed beyond fetch's default; status reported as-is
|
|
19
|
+
*
|
|
20
|
+
* @module @claude-flow/cli/mcp-tools/http-fetch
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
|
+
const MAX_TIMEOUT_MS = 60_000;
|
|
24
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
|
|
25
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MB
|
|
26
|
+
const DEFAULT_USER_AGENT = 'ruflo-http-fetch/1.0';
|
|
27
|
+
const FORBIDDEN_HEADER_PREFIXES = ['x-auth-', 'x-api-key'];
|
|
28
|
+
const FORBIDDEN_HEADERS_EXACT = ['authorization', 'cookie', 'set-cookie', 'proxy-authorization'];
|
|
29
|
+
export class HttpFetchValidationError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(message, code) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.name = 'HttpFetchValidationError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decide whether the URL is permitted under the default secure-by-default
|
|
39
|
+
* allowlist. Block file://, ftp://, RFC-1918 private addresses, loopback,
|
|
40
|
+
* link-local — unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 is set.
|
|
41
|
+
*/
|
|
42
|
+
export function validateUrl(rawUrl) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(rawUrl);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');
|
|
49
|
+
}
|
|
50
|
+
const proto = parsed.protocol.toLowerCase();
|
|
51
|
+
if (proto !== 'http:' && proto !== 'https:') {
|
|
52
|
+
throw new HttpFetchValidationError(`protocol ${parsed.protocol} not allowed (only http: and https:)`, 'FORBIDDEN_PROTOCOL');
|
|
53
|
+
}
|
|
54
|
+
const host = parsed.hostname.toLowerCase();
|
|
55
|
+
const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
|
|
56
|
+
if (!allowPrivate && isPrivateOrLoopback(host)) {
|
|
57
|
+
throw new HttpFetchValidationError(`host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`, 'PRIVATE_ADDRESS');
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
function isPrivateOrLoopback(host) {
|
|
62
|
+
if (host === 'localhost' || host === 'localhost.localdomain')
|
|
63
|
+
return true;
|
|
64
|
+
// IPv6 loopback
|
|
65
|
+
if (host === '::1' || host === '[::1]')
|
|
66
|
+
return true;
|
|
67
|
+
// IPv4 numeric checks
|
|
68
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
69
|
+
if (m) {
|
|
70
|
+
const a = Number(m[1]);
|
|
71
|
+
const b = Number(m[2]);
|
|
72
|
+
if (a === 0)
|
|
73
|
+
return true; // 0.0.0.0/8
|
|
74
|
+
if (a === 127)
|
|
75
|
+
return true; // loopback
|
|
76
|
+
if (a === 10)
|
|
77
|
+
return true; // RFC1918
|
|
78
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
79
|
+
return true; // RFC1918
|
|
80
|
+
if (a === 192 && b === 168)
|
|
81
|
+
return true; // RFC1918
|
|
82
|
+
if (a === 169 && b === 254)
|
|
83
|
+
return true; // link-local
|
|
84
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
85
|
+
return true; // CGNAT
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// IPv6 bracketed addresses and other special forms — be conservative and
|
|
89
|
+
// accept only public-looking literals. Reject obvious private/local forms.
|
|
90
|
+
if (host.startsWith('fc') || host.startsWith('fd'))
|
|
91
|
+
return true; // fc00::/7 ULA
|
|
92
|
+
if (host.startsWith('fe80:'))
|
|
93
|
+
return true; // link-local
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
export function validateHeaders(headers) {
|
|
97
|
+
const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
100
|
+
const lower = key.toLowerCase();
|
|
101
|
+
if (!allowAuth) {
|
|
102
|
+
if (FORBIDDEN_HEADERS_EXACT.includes(lower)) {
|
|
103
|
+
throw new HttpFetchValidationError(`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`, 'FORBIDDEN_HEADER');
|
|
104
|
+
}
|
|
105
|
+
if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {
|
|
106
|
+
throw new HttpFetchValidationError(`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`, 'FORBIDDEN_HEADER');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (typeof value !== 'string') {
|
|
110
|
+
throw new HttpFetchValidationError(`header "${key}" must be a string`, 'INVALID_HEADER_VALUE');
|
|
111
|
+
}
|
|
112
|
+
out[key] = value;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function clampNumber(raw, defaultValue, max) {
|
|
117
|
+
if (raw === undefined || raw === null)
|
|
118
|
+
return defaultValue;
|
|
119
|
+
const n = Number(raw);
|
|
120
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
121
|
+
return defaultValue;
|
|
122
|
+
return Math.min(Math.floor(n), max);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pure execution path so tests can call it without going through the MCP
|
|
126
|
+
* dispatcher. Returns a result object (does not throw on validation failure
|
|
127
|
+
* — it returns success: false with an errorCode).
|
|
128
|
+
*/
|
|
129
|
+
export async function httpFetchExecute(input) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
const url = String(input.url ?? '');
|
|
132
|
+
const method = String(input.method ?? 'GET').toUpperCase();
|
|
133
|
+
if (!['GET', 'POST', 'HEAD'].includes(method)) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
status: 0,
|
|
137
|
+
statusText: '',
|
|
138
|
+
headers: {},
|
|
139
|
+
body: '',
|
|
140
|
+
bodyTruncated: false,
|
|
141
|
+
bytesRead: 0,
|
|
142
|
+
durationMs: Date.now() - startedAt,
|
|
143
|
+
url,
|
|
144
|
+
method,
|
|
145
|
+
error: `method ${method} not allowed (GET, POST, HEAD only)`,
|
|
146
|
+
errorCode: 'INVALID_METHOD',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = validateUrl(url);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const err = e;
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
status: 0,
|
|
158
|
+
statusText: '',
|
|
159
|
+
headers: {},
|
|
160
|
+
body: '',
|
|
161
|
+
bodyTruncated: false,
|
|
162
|
+
bytesRead: 0,
|
|
163
|
+
durationMs: Date.now() - startedAt,
|
|
164
|
+
url,
|
|
165
|
+
method,
|
|
166
|
+
error: err.message,
|
|
167
|
+
errorCode: err.code ?? 'VALIDATION_ERROR',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
let headers;
|
|
171
|
+
try {
|
|
172
|
+
const raw = (input.headers ?? {});
|
|
173
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
174
|
+
throw new HttpFetchValidationError('headers must be an object', 'INVALID_HEADERS');
|
|
175
|
+
}
|
|
176
|
+
headers = validateHeaders(raw);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
const err = e;
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
status: 0,
|
|
183
|
+
statusText: '',
|
|
184
|
+
headers: {},
|
|
185
|
+
body: '',
|
|
186
|
+
bodyTruncated: false,
|
|
187
|
+
bytesRead: 0,
|
|
188
|
+
durationMs: Date.now() - startedAt,
|
|
189
|
+
url,
|
|
190
|
+
method,
|
|
191
|
+
error: err.message,
|
|
192
|
+
errorCode: err.code ?? 'VALIDATION_ERROR',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Default UA if not set
|
|
196
|
+
const hasUA = Object.keys(headers).some((k) => k.toLowerCase() === 'user-agent');
|
|
197
|
+
if (!hasUA)
|
|
198
|
+
headers['User-Agent'] = DEFAULT_USER_AGENT;
|
|
199
|
+
const timeoutMs = clampNumber(input.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
200
|
+
const maxResponseBytes = clampNumber(input.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES);
|
|
201
|
+
const body = input.body !== undefined ? String(input.body) : undefined;
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(parsed.toString(), {
|
|
206
|
+
method,
|
|
207
|
+
headers,
|
|
208
|
+
body: method === 'GET' || method === 'HEAD' ? undefined : body,
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
// Keep redirect default ('follow') — caller sees final status/url.
|
|
211
|
+
});
|
|
212
|
+
// Drain the body with byte budget. Use array-buffer first to bound size
|
|
213
|
+
// deterministically before decoding to UTF-8.
|
|
214
|
+
const reader = response.body?.getReader();
|
|
215
|
+
let bytesRead = 0;
|
|
216
|
+
let bodyTruncated = false;
|
|
217
|
+
const chunks = [];
|
|
218
|
+
if (reader) {
|
|
219
|
+
// eslint-disable-next-line no-constant-condition
|
|
220
|
+
while (true) {
|
|
221
|
+
const { value, done } = await reader.read();
|
|
222
|
+
if (done)
|
|
223
|
+
break;
|
|
224
|
+
if (value) {
|
|
225
|
+
if (bytesRead + value.byteLength > maxResponseBytes) {
|
|
226
|
+
const remaining = Math.max(0, maxResponseBytes - bytesRead);
|
|
227
|
+
if (remaining > 0)
|
|
228
|
+
chunks.push(value.slice(0, remaining));
|
|
229
|
+
bytesRead += remaining;
|
|
230
|
+
bodyTruncated = true;
|
|
231
|
+
try {
|
|
232
|
+
await reader.cancel('body-truncated');
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
chunks.push(value);
|
|
238
|
+
bytesRead += value.byteLength;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const combined = new Uint8Array(bytesRead);
|
|
243
|
+
let offset = 0;
|
|
244
|
+
for (const c of chunks) {
|
|
245
|
+
combined.set(c, offset);
|
|
246
|
+
offset += c.byteLength;
|
|
247
|
+
}
|
|
248
|
+
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
249
|
+
const bodyText = decoder.decode(combined);
|
|
250
|
+
const outHeaders = {};
|
|
251
|
+
response.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
status: response.status,
|
|
255
|
+
statusText: response.statusText,
|
|
256
|
+
headers: outHeaders,
|
|
257
|
+
body: bodyText,
|
|
258
|
+
bodyTruncated,
|
|
259
|
+
bytesRead,
|
|
260
|
+
durationMs: Date.now() - startedAt,
|
|
261
|
+
url: parsed.toString(),
|
|
262
|
+
method,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
267
|
+
const aborted = msg.includes('aborted') || e?.name === 'AbortError';
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
status: 0,
|
|
271
|
+
statusText: '',
|
|
272
|
+
headers: {},
|
|
273
|
+
body: '',
|
|
274
|
+
bodyTruncated: false,
|
|
275
|
+
bytesRead: 0,
|
|
276
|
+
durationMs: Date.now() - startedAt,
|
|
277
|
+
url: parsed.toString(),
|
|
278
|
+
method,
|
|
279
|
+
error: aborted ? `request aborted after ${timeoutMs}ms timeout` : msg,
|
|
280
|
+
errorCode: aborted ? 'TIMEOUT' : 'FETCH_ERROR',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export const httpFetchTools = [
|
|
288
|
+
{
|
|
289
|
+
name: 'http_fetch',
|
|
290
|
+
description: 'ADR-164 §5.1.8 — HTTP probe primitive for business-pod ops benches (synthetic 200/500 endpoint checks, third-party status pages). Default-secure: blocks file://, ftp://, RFC-1918 / loopback / link-local hosts unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1, and rejects Authorization / Cookie / X-Auth-* headers unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1. Hard 30s timeout (60s ceiling), response truncated to 256 KB (1 MB ceiling), default User-Agent ruflo-http-fetch/1.0. Use when a pod or smoke contract needs a guarded HTTP probe — calling Node fetch() directly is wrong because it skips the URL allowlist and header sanitization that ADR-164 mandates for autopilot mode. Pair with the ops-pod bench in plugins/ruflo-business-pods/templates/ops.json (the §4.4 synthetic-endpoint test).',
|
|
291
|
+
category: 'business-pods',
|
|
292
|
+
tags: ['business-pods', 'http', 'fetch', 'adr-164', 'security', 'ops-pod'],
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
url: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'Absolute http:// or https:// URL. file://, ftp://, RFC-1918 / loopback / link-local blocked by default.',
|
|
299
|
+
},
|
|
300
|
+
method: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
enum: ['GET', 'POST', 'HEAD'],
|
|
303
|
+
description: 'HTTP method. Defaults to GET.',
|
|
304
|
+
},
|
|
305
|
+
timeoutMs: {
|
|
306
|
+
type: 'number',
|
|
307
|
+
description: 'Hard timeout in milliseconds. Default 30000, max 60000.',
|
|
308
|
+
},
|
|
309
|
+
maxResponseBytes: {
|
|
310
|
+
type: 'number',
|
|
311
|
+
description: 'Max body bytes to read before truncation. Default 262144 (256 KB), max 1048576 (1 MB).',
|
|
312
|
+
},
|
|
313
|
+
headers: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
description: 'Extra request headers. Authorization / Cookie / X-Auth-* blocked unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1.',
|
|
316
|
+
},
|
|
317
|
+
body: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Request body for POST. Ignored for GET / HEAD.',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
required: ['url'],
|
|
323
|
+
},
|
|
324
|
+
handler: async (input) => {
|
|
325
|
+
return await httpFetchExecute(input);
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
//# sourceMappingURL=http-fetch-tools.js.map
|
|
@@ -27,4 +27,7 @@ export { autopilotTools } from './autopilot-tools.js';
|
|
|
27
27
|
export { metaharnessTools } from './metaharness-tools.js';
|
|
28
28
|
export { testgenTools } from './testgen-tools.js';
|
|
29
29
|
export { agenticowTools } from './agenticow-tools.js';
|
|
30
|
+
export { agentbbsTools } from './agentbbs-tools.js';
|
|
31
|
+
export { businessPodTools } from './business-pod-tools.js';
|
|
32
|
+
export { httpFetchTools } from './http-fetch-tools.js';
|
|
30
33
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -29,4 +29,10 @@ export { metaharnessTools } from './metaharness-tools.js';
|
|
|
29
29
|
export { testgenTools } from './testgen-tools.js';
|
|
30
30
|
// agenticow@~0.2.3 — Copy-On-Write memory branching (162-byte branches)
|
|
31
31
|
export { agenticowTools } from './agenticow-tools.js';
|
|
32
|
+
// ADR-164 — AgentBBS federated business-domain BBS rooms (Phase 1)
|
|
33
|
+
export { agentbbsTools } from './agentbbs-tools.js';
|
|
34
|
+
// ADR-164 Phase 2 — Business-pod template validation
|
|
35
|
+
export { businessPodTools } from './business-pod-tools.js';
|
|
36
|
+
// ADR-164 Phase 4 §5.1.8 — http_fetch (secure-by-default HTTP probe)
|
|
37
|
+
export { httpFetchTools } from './http-fetch-tools.js';
|
|
32
38
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"yaml": "^2.8.0"
|
|
107
107
|
},
|
|
108
108
|
"optionalDependencies": {
|
|
109
|
+
"agentbbs": "~0.1.0",
|
|
109
110
|
"agenticow": "~0.2.3",
|
|
110
111
|
"@claude-flow/aidefence": "^3.0.2",
|
|
111
112
|
"@claude-flow/codex": "^3.0.0-alpha.8",
|