osborn 0.9.149 → 0.9.151
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/dist/claude-llm.js +74 -5
- package/dist/index.js +446 -7
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -14,7 +14,7 @@ import { statusManager } from './status-manager.js';
|
|
|
14
14
|
import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
|
|
15
15
|
import { getIndexPath } from './summary-index.js';
|
|
16
16
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
|
-
import { join, dirname } from 'node:path';
|
|
17
|
+
import { join, dirname, resolve, basename } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
// Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
|
|
@@ -183,6 +183,11 @@ export const NAMED_AGENTS = {
|
|
|
183
183
|
'Gather information the main agent needs to answer the user\'s question or make a decision.',
|
|
184
184
|
'You are a scout — go find things, read them carefully, and report back.',
|
|
185
185
|
'',
|
|
186
|
+
'## Grounding — check the session index first',
|
|
187
|
+
'Before researching, locate the session index (search-index.txt — a compact line-per-message log of this mission, under .claude/projects/<slug>/osb/<session>/; if several exist pick the most recently modified) and Grep it for the topic you are about to investigate. Read ONLY the matching slice, never the whole file.',
|
|
188
|
+
'Purpose: find what has ALREADY been decided, answered, or ruled out so you do not re-research a settled question. If the index already establishes the answer, report that (with the index reference) instead of redoing the work.',
|
|
189
|
+
'If you cannot find the index, proceed normally — this is an optimization, not a hard dependency.',
|
|
190
|
+
'',
|
|
186
191
|
'## How to work',
|
|
187
192
|
'1. Understand what information is needed and why.',
|
|
188
193
|
'2. Search broadly first (Glob, Grep, WebSearch), then read deeply (Read specific files).',
|
|
@@ -278,6 +283,10 @@ export const NAMED_AGENTS = {
|
|
|
278
283
|
'Handle ALL file operations — code, config, documentation, scripts, data files.',
|
|
279
284
|
'You are the only agent that writes. The main agent and reasoner produce plans; you execute them.',
|
|
280
285
|
'',
|
|
286
|
+
'## Grounding — consult the session index (light touch)',
|
|
287
|
+
'Before editing, locate the session index (search-index.txt under .claude/projects/<slug>/osb/<session>/; newest if several) and Grep it ONLY for: (a) the files/symbols you are about to change, and (b) any recorded DECISIONS or known GOTCHAS relevant to this change. Read only the matching lines — do NOT read the whole index (thousands of lines). This is a lighter dose than the reviewer: a targeted lookup.',
|
|
288
|
+
'If a decision or gotcha contradicts your task, STOP and report to the main agent before editing. If you find nothing or no index exists, proceed normally.',
|
|
289
|
+
'',
|
|
281
290
|
'## VERIFY-FIRST workflow (mandatory)',
|
|
282
291
|
'',
|
|
283
292
|
'### Step 1: Verify assumptions',
|
|
@@ -317,7 +326,7 @@ export const NAMED_AGENTS = {
|
|
|
317
326
|
'CI failures, checking compilation errors, verifying that a change did not break anything.',
|
|
318
327
|
'Returns structured pass/fail results with exact output — does NOT edit files.',
|
|
319
328
|
].join(' '),
|
|
320
|
-
tools: ['Bash', 'Read', 'Glob', 'Grep'],
|
|
329
|
+
tools: ['Bash', 'Read', 'Glob', 'Grep', 'Write', 'Edit'],
|
|
321
330
|
model: 'sonnet',
|
|
322
331
|
prompt: [
|
|
323
332
|
'You are Osborn\'s tester agent. Your job is running tests and builds, then reporting results.',
|
|
@@ -326,6 +335,11 @@ export const NAMED_AGENTS = {
|
|
|
326
335
|
'Execute test suites, build commands, and linters. Interpret failures clearly.',
|
|
327
336
|
'You are a quality gate — find out whether the code works, and say exactly what broke.',
|
|
328
337
|
'',
|
|
338
|
+
'## Grounding — consult shared context before writing or running tests',
|
|
339
|
+
'Before deciding what to test, locate the session index (search-index.txt under .claude/projects/<slug>/osb/<session>/; newest if several) and Grep it for the changes/work under test. Also check any project docs and known-issues files (e.g. CONVENTIONS.md, docs/, a known-issues or gotchas doc if present) for: (a) KNOWN ISSUES and gotchas already recorded, and (b) what behavior is ALREADY covered by existing tests.',
|
|
340
|
+
'Purpose: target regression coverage at real GAPS and known-risk areas rather than testing blind or duplicating coverage — and stay IN SYNC with the reviewer, which reads the same sources.',
|
|
341
|
+
'Read only the relevant slice of the index, never the whole file. If you find nothing or no index/docs exist, proceed normally — this is an optimization, not a hard dependency.',
|
|
342
|
+
'',
|
|
329
343
|
'## Backward-compatibility / regression mandate (CRITICAL)',
|
|
330
344
|
'Existing test suites MUST still pass — any pre-existing test that breaks is a BLOCKER; report it as such.',
|
|
331
345
|
'The public API surface (function signatures, exported types, return shapes, behavior) must NOT silently change.',
|
|
@@ -354,6 +368,16 @@ export const NAMED_AGENTS = {
|
|
|
354
368
|
'- REGRESSIONS / COMPAT BREAKS: explicit list of any pre-existing tests that now fail or API changes that could break existing callers — tag each as BLOCKER',
|
|
355
369
|
'- What you checked but found to be unrelated',
|
|
356
370
|
'',
|
|
371
|
+
'## Backward-compatibility testing & building the test library',
|
|
372
|
+
'GROW THE LIBRARY OVER TIME: where coverage is missing for the behavior being verified,',
|
|
373
|
+
'CREATE a targeted regression test so the suite accumulates over time. If NO test suite or',
|
|
374
|
+
'test infrastructure exists yet, establish a MINIMAL one — a single test file plus the',
|
|
375
|
+
'smallest runner wiring needed — do NOT stand up a heavy framework; keep it small and incremental.',
|
|
376
|
+
'',
|
|
377
|
+
'HARD RESTRICTION: you may ONLY write TEST files — files whose names contain `.test.` or `.spec.`,',
|
|
378
|
+
'or files located under a `__tests__/` or `tests/` directory.',
|
|
379
|
+
'NEVER write source, config, or other files. The write-gate enforces this and will deny any non-test path.',
|
|
380
|
+
'',
|
|
357
381
|
'## What NOT to do',
|
|
358
382
|
'- Do NOT edit or write production files — report failures so the writer agent can fix them',
|
|
359
383
|
'- Do NOT run destructive commands (no rm, no git push, no npm publish)',
|
|
@@ -382,6 +406,18 @@ export const NAMED_AGENTS = {
|
|
|
382
406
|
'Turn a vague or large request into a precise, ordered implementation plan the writer can execute',
|
|
383
407
|
'step by step without guessing. You are the bridge between "what" and "how".',
|
|
384
408
|
'',
|
|
409
|
+
'## Grounding — plan against what already exists',
|
|
410
|
+
'Before drafting a plan, locate the session index (search-index.txt under .claude/projects/<slug>/osb/<session>/; newest if several) and Grep it for prior DECISIONS, constraints, and known GOTCHAS relevant to the task. Read only the matching slice, never the whole file.',
|
|
411
|
+
'Purpose: make the plan fit what has already been decided or tried — do not propose an approach the mission already ruled out. If a prior decision conflicts with the obvious plan, surface it in the plan rather than silently contradicting it.',
|
|
412
|
+
'If you cannot find the index, proceed normally.',
|
|
413
|
+
'',
|
|
414
|
+
'## Documentation at delivery milestones',
|
|
415
|
+
'Plans should account for documentation reconciliation at the point work is DELIVERED — when it ships,',
|
|
416
|
+
'goes live, or reaches a release milestone. Include a step (or an explicit note in VERIFY) to check',
|
|
417
|
+
'whether existing documentation still reflects what is changing, and flag it as a task if updates are',
|
|
418
|
+
'needed. Do NOT schedule doc updates for every incremental step; anchor them to the delivery boundary',
|
|
419
|
+
'so documentation stays stable between releases and does not silently drift out of sync when the work ships.',
|
|
420
|
+
'',
|
|
385
421
|
'## How to work',
|
|
386
422
|
'1. Read enough of the codebase to understand the current structure (Glob, Grep, Read).',
|
|
387
423
|
'2. Identify every file that needs to change and why.',
|
|
@@ -439,10 +475,18 @@ export const NAMED_AGENTS = {
|
|
|
439
475
|
'## Maintain documentation',
|
|
440
476
|
'You MAY create and maintain project standards and conventions files. Specifically: adopt an existing',
|
|
441
477
|
'standards doc if one is found, or create one (e.g. CONVENTIONS.md, docs/standards.md) when none exists',
|
|
442
|
-
'and the review reveals patterns worth recording.
|
|
478
|
+
'and the review reveals patterns worth recording.',
|
|
479
|
+
'',
|
|
480
|
+
'WHEN to reconcile docs: at the moment work is being DELIVERED — when a unit of work ships, goes live,',
|
|
481
|
+
'or reaches a release milestone. That is the checkpoint to verify whether existing documentation still',
|
|
482
|
+
'reflects what actually changed and update it if it has drifted. Do NOT update docs on every small',
|
|
483
|
+
'incremental change; wait for the natural delivery boundary so docs stay stable between releases.',
|
|
484
|
+
'Between delivery milestones, note discrepancies as findings in your verdict (MINOR or NIT) rather',
|
|
485
|
+
'than immediately rewriting documentation.',
|
|
486
|
+
'',
|
|
443
487
|
'RESTRICTION: you may ONLY write files with documentation extensions: .md, .markdown, .mdx, .txt, .rst, .adoc.',
|
|
444
|
-
'You must NEVER write
|
|
445
|
-
'
|
|
488
|
+
'You must NEVER write source, config, or executable files — the write gate enforces this at the system',
|
|
489
|
+
'level and will deny any such attempt.',
|
|
446
490
|
'',
|
|
447
491
|
'## Backward-compatibility mandate',
|
|
448
492
|
'For every change, explicitly verify:',
|
|
@@ -453,6 +497,15 @@ export const NAMED_AGENTS = {
|
|
|
453
497
|
'- Existing callers and consumers of the changed code still work',
|
|
454
498
|
'Tag any violation BLOCKER — silent compat breaks are the hardest bugs to catch after the fact.',
|
|
455
499
|
'',
|
|
500
|
+
'## Regression test suite — run and correlate',
|
|
501
|
+
'If a backward-compatibility/regression test suite exists in the project (any test/ or __tests__/',
|
|
502
|
+
'directory, *.test.* or *.spec.* files, or a test script in package.json), RUN IT as part of your',
|
|
503
|
+
'review using Bash. Correlate the results into your verdict:',
|
|
504
|
+
'- A passing suite with no regressions: note it in WHAT IT CHECKED-AND-CLEARED.',
|
|
505
|
+
'- Any pre-existing test that now fails is a BLOCKER — include it as an ISSUES entry tagged BLOCKER.',
|
|
506
|
+
'Do not skip this step when a suite exists; a failing regression test is a hard blocker regardless',
|
|
507
|
+
'of whether the diff looks clean.',
|
|
508
|
+
'',
|
|
456
509
|
'## How to work',
|
|
457
510
|
'0. **Get the diff first (MANDATORY):** Run `git diff HEAD~1 HEAD --stat` then `git diff HEAD~1 HEAD`.',
|
|
458
511
|
' Build your entire review around what ACTUALLY changed — not the writer\'s narrative alone.',
|
|
@@ -1621,6 +1674,22 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1621
1674
|
console.log(`📝 Reviewer doc write allowed: ${reviewerPath}`);
|
|
1622
1675
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
1623
1676
|
}
|
|
1677
|
+
// Tester agent: ONLY test files allowed — fail closed
|
|
1678
|
+
if (agentType === 'tester') {
|
|
1679
|
+
const testerPath = String(toolInput.file_path || '');
|
|
1680
|
+
const STRICT_TEST_FILE = /\.(test|spec)\.[jt]sx?$/;
|
|
1681
|
+
const resolvedBase = testerPath ? basename(resolve(testerPath)) : '';
|
|
1682
|
+
if (!testerPath || !STRICT_TEST_FILE.test(resolvedBase)) {
|
|
1683
|
+
const reason = testerPath
|
|
1684
|
+
? `Tester write denied: ${testerPath} is not a test file (basename must match .test.ts/tsx/js/jsx or .spec.ts/tsx/js/jsx). Tester may only write test files.`
|
|
1685
|
+
: 'Tester write denied: could not determine target file path. Failing closed.';
|
|
1686
|
+
console.log(`🚫 Tester write blocked: ${testerPath || '(no path)'} — not a test file`);
|
|
1687
|
+
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
1688
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
1689
|
+
}
|
|
1690
|
+
console.log(`🧪 Tester test-file write allowed: ${testerPath}`);
|
|
1691
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
1692
|
+
}
|
|
1624
1693
|
// All other agents (main, researcher, reasoner, etc.): workspace only
|
|
1625
1694
|
const filePath = String(toolInput.file_path || '');
|
|
1626
1695
|
if (filePath && !filePath.includes('/osb/') && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
|
package/dist/index.js
CHANGED
|
@@ -9,12 +9,13 @@ initializeLogger({ pretty: true, level: 'info' });
|
|
|
9
9
|
// Each resumed query() adds listeners to the shared signal; default limit is 10
|
|
10
10
|
import { setMaxListeners } from 'node:events';
|
|
11
11
|
setMaxListeners(50);
|
|
12
|
-
import { createServer } from 'http';
|
|
12
|
+
import { createServer, request as httpRequest } from 'http';
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync, rmSync, statSync, utimesSync, createWriteStream } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { randomUUID, randomBytes } from 'node:crypto';
|
|
18
|
+
import { createConnection as netConnect } from 'node:net';
|
|
18
19
|
import { createRequire } from 'node:module';
|
|
19
20
|
// 0.9.71: createRequire for resolving package.json versions inside ESM
|
|
20
21
|
const __sdkVersionRequire = createRequire(import.meta.url);
|
|
@@ -258,6 +259,48 @@ let leaveRoomHook = null;
|
|
|
258
259
|
// as the existing fetch-log/save-log flow so we don't ship Supabase credentials
|
|
259
260
|
// to the Fly machine.
|
|
260
261
|
let bugReportHook = null;
|
|
262
|
+
// Module-scope hook wired by main() once startCodeServer() is defined in its
|
|
263
|
+
// closure. The /editor route in startApiServer() calls this to start the IDE
|
|
264
|
+
// in the background without depending on main()'s closure directly.
|
|
265
|
+
let codeServerStartHook = null;
|
|
266
|
+
// ── IDE reverse-proxy (module scope — shared between startApiServer + main) ──
|
|
267
|
+
// code-server runs on 127.0.0.1:8300; the agent proxies it through its own
|
|
268
|
+
// public HTTP server so no Cloudflare tunnel is needed.
|
|
269
|
+
const IDE_TARGET = 'http://127.0.0.1:8300';
|
|
270
|
+
// Route prefixes that belong to the agent itself. The proxy fall-through checks
|
|
271
|
+
// this list FIRST so it never intercepts an agent API path.
|
|
272
|
+
const AGENT_ROUTE_PREFIXES = [
|
|
273
|
+
'/health', '/sessions', '/skills', '/agents', '/canvas', '/canvas-token',
|
|
274
|
+
'/canvas-stream', '/events', '/room-code', '/connect-room', '/leave-room',
|
|
275
|
+
'/report-bug', '/restart', '/tts', '/webhook', '/sessions/export',
|
|
276
|
+
'/sessions/import', '/sessions/manifest',
|
|
277
|
+
'/editor',
|
|
278
|
+
];
|
|
279
|
+
// Set true when code-server is confirmed ready; false when stopped/not running.
|
|
280
|
+
// Proxy returns 404 while this is false.
|
|
281
|
+
let ideProxyEnabled = false;
|
|
282
|
+
// Bumped on every proxied HTTP request and WS upgrade so the idle watcher sees
|
|
283
|
+
// real editor activity rather than code-server log noise.
|
|
284
|
+
let ideLastProxiedActivity = 0;
|
|
285
|
+
// Per-session cookie token minted when code-server becomes ready. The proxy
|
|
286
|
+
// gate checks this token via the osborn_ide cookie — only requests carrying a
|
|
287
|
+
// valid cookie are forwarded, so a forgotten agent route can never be silently
|
|
288
|
+
// swallowed by the IDE proxy. Cleared in stopIde().
|
|
289
|
+
let ideSessionToken = null;
|
|
290
|
+
// Helper: parse the osborn_ide cookie from a request and return true iff it
|
|
291
|
+
// matches the current ideSessionToken (and a token exists).
|
|
292
|
+
function hasValidIdeCookie(req) {
|
|
293
|
+
if (!ideSessionToken)
|
|
294
|
+
return false;
|
|
295
|
+
const cookieHeader = req.headers.cookie || '';
|
|
296
|
+
for (const part of cookieHeader.split(';')) {
|
|
297
|
+
const [name, ...rest] = part.trim().split('=');
|
|
298
|
+
if (name.trim() === 'osborn_ide' && rest.join('=').trim() === ideSessionToken) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
261
304
|
const canvasClients = new Set();
|
|
262
305
|
let latestCanvasShow = null; // last 'show' so a reconnecting canvas resyncs its visual
|
|
263
306
|
function pushCanvas(evt) {
|
|
@@ -1430,6 +1473,77 @@ function startApiServer(workingDir, port) {
|
|
|
1430
1473
|
}
|
|
1431
1474
|
return;
|
|
1432
1475
|
}
|
|
1476
|
+
// ── /editor entry point ──────────────────────────────────────────────────
|
|
1477
|
+
// Explicit door into the IDE. When code-server is ready this route mints the
|
|
1478
|
+
// osborn_ide session cookie and 302s to / (where the proxy then forwards). If
|
|
1479
|
+
// code-server is not yet running, it starts it in the background and returns a
|
|
1480
|
+
// self-refreshing "Starting…" page so the user naturally re-hits /editor every
|
|
1481
|
+
// 3 s until ready.
|
|
1482
|
+
if (url.pathname === '/editor' && req.method === 'GET') {
|
|
1483
|
+
// AUTH GATE (future): validate key / signed session before minting the cookie
|
|
1484
|
+
if (ideProxyEnabled && ideSessionToken) {
|
|
1485
|
+
// code-server is up — set cookie and redirect to root (code-server's asset base)
|
|
1486
|
+
res.writeHead(302, {
|
|
1487
|
+
'Set-Cookie': `osborn_ide=${ideSessionToken}; Path=/; HttpOnly; SameSite=Lax`,
|
|
1488
|
+
'Location': '/',
|
|
1489
|
+
});
|
|
1490
|
+
res.end();
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
// Not yet running — kick off start in the background, show "Starting…" page
|
|
1494
|
+
if (codeServerStartHook) {
|
|
1495
|
+
codeServerStartHook().catch((err) => {
|
|
1496
|
+
console.error('❌ startCodeServer() from /editor failed:', err);
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
res.writeHead(200, {
|
|
1500
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1501
|
+
'Cache-Control': 'no-store, must-revalidate',
|
|
1502
|
+
'Pragma': 'no-cache',
|
|
1503
|
+
});
|
|
1504
|
+
res.end(`<!DOCTYPE html><html><head>
|
|
1505
|
+
<meta charset="utf-8">
|
|
1506
|
+
<meta http-equiv="refresh" content="3;url=/editor">
|
|
1507
|
+
<title>Starting editor…</title>
|
|
1508
|
+
<style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#1e1e1e;color:#ccc}</style>
|
|
1509
|
+
</head><body><p>Starting your editor…</p></body></html>`);
|
|
1510
|
+
}
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
1514
|
+
// ── IDE reverse proxy (cookie-gated fall-through) ────────────────────────
|
|
1515
|
+
// Forwards to code-server ONLY when:
|
|
1516
|
+
// 1. ideProxyEnabled is true (code-server is confirmed ready), AND
|
|
1517
|
+
// 2. the request carries a valid osborn_ide session cookie (primary gate), AND
|
|
1518
|
+
// 3. the path is not an agent route (secondary backstop).
|
|
1519
|
+
// A missing/invalid cookie means the request came from somewhere other than
|
|
1520
|
+
// the /editor door — refuse it so a forgotten agent route is never swallowed.
|
|
1521
|
+
if (ideProxyEnabled &&
|
|
1522
|
+
hasValidIdeCookie(req) &&
|
|
1523
|
+
!AGENT_ROUTE_PREFIXES.some(p => url.pathname === p || url.pathname.startsWith(p + '/'))) {
|
|
1524
|
+
ideLastProxiedActivity = Date.now();
|
|
1525
|
+
const targetUrl = new URL(req.url || '/', IDE_TARGET);
|
|
1526
|
+
const options = {
|
|
1527
|
+
hostname: '127.0.0.1',
|
|
1528
|
+
port: 8300,
|
|
1529
|
+
path: targetUrl.pathname + (targetUrl.search || ''),
|
|
1530
|
+
method: req.method,
|
|
1531
|
+
headers: { ...req.headers, host: '127.0.0.1:8300' },
|
|
1532
|
+
};
|
|
1533
|
+
const proxyReq = httpRequest(options, (proxyRes) => {
|
|
1534
|
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
1535
|
+
proxyRes.pipe(res);
|
|
1536
|
+
});
|
|
1537
|
+
proxyReq.on('error', () => {
|
|
1538
|
+
if (!res.headersSent) {
|
|
1539
|
+
res.writeHead(502);
|
|
1540
|
+
res.end('IDE proxy error');
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
req.pipe(proxyReq);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
1433
1547
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1434
1548
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1435
1549
|
});
|
|
@@ -1467,11 +1581,53 @@ function startApiServer(workingDir, port) {
|
|
|
1467
1581
|
};
|
|
1468
1582
|
cleanStaleUploadDirs();
|
|
1469
1583
|
setInterval(cleanStaleUploadDirs, 10 * 60 * 1000);
|
|
1470
|
-
//
|
|
1471
|
-
// a
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1584
|
+
// WebSocket upgrade handler: forward to code-server when the IDE proxy is
|
|
1585
|
+
// active, the request carries a valid osborn_ide session cookie, and the path
|
|
1586
|
+
// is not an agent route. code-server's terminal and editor are websocket-heavy
|
|
1587
|
+
// — this must work for the integrated terminal to connect.
|
|
1588
|
+
// Reject all other upgrades (meeting audio moved to polling in 0.9.xx).
|
|
1589
|
+
server.on('upgrade', (req, socket, head) => {
|
|
1590
|
+
const upgradePath = new URL(req.url || '/', `http://localhost`).pathname;
|
|
1591
|
+
const isAgentRoute = AGENT_ROUTE_PREFIXES.some(p => upgradePath === p || upgradePath.startsWith(p + '/'));
|
|
1592
|
+
if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {
|
|
1593
|
+
ideLastProxiedActivity = Date.now();
|
|
1594
|
+
// Raw TCP tunnel: connect to code-server and splice the socket.
|
|
1595
|
+
// Pause the client socket before initiating the connect so that any
|
|
1596
|
+
// bytes the browser sends before the connect callback fires are buffered
|
|
1597
|
+
// by Node rather than silently dropped — the classic silent-hang shape
|
|
1598
|
+
// for a websocket terminal on a slow connect.
|
|
1599
|
+
socket.pause();
|
|
1600
|
+
const conn = netConnect(8300, '127.0.0.1', () => {
|
|
1601
|
+
// Forward the original HTTP upgrade request headers.
|
|
1602
|
+
const headerLines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
1603
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1604
|
+
if (k.toLowerCase() === 'host')
|
|
1605
|
+
continue;
|
|
1606
|
+
if (Array.isArray(v)) {
|
|
1607
|
+
for (const vv of v)
|
|
1608
|
+
headerLines.push(`${k}: ${vv}`);
|
|
1609
|
+
}
|
|
1610
|
+
else if (v != null)
|
|
1611
|
+
headerLines.push(`${k}: ${v}`);
|
|
1612
|
+
}
|
|
1613
|
+
headerLines.push('host: 127.0.0.1:8300');
|
|
1614
|
+
conn.write(headerLines.join('\r\n') + '\r\n\r\n');
|
|
1615
|
+
if (head && head.length)
|
|
1616
|
+
conn.write(head);
|
|
1617
|
+
conn.pipe(socket);
|
|
1618
|
+
socket.pipe(conn);
|
|
1619
|
+
socket.resume();
|
|
1620
|
+
});
|
|
1621
|
+
conn.on('error', () => { try {
|
|
1622
|
+
socket.destroy();
|
|
1623
|
+
}
|
|
1624
|
+
catch { } });
|
|
1625
|
+
socket.on('error', () => { try {
|
|
1626
|
+
conn.destroy();
|
|
1627
|
+
}
|
|
1628
|
+
catch { } });
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1475
1631
|
socket.destroy();
|
|
1476
1632
|
});
|
|
1477
1633
|
server.on('error', (err) => {
|
|
@@ -1990,6 +2146,19 @@ async function main() {
|
|
|
1990
2146
|
// the fresh session's latch was never opened by the name).
|
|
1991
2147
|
const meetingSpeakers = new Set();
|
|
1992
2148
|
let activeMeetingPoller = null; // Transcript poller bound to that bot
|
|
2149
|
+
// ── IDE (code-server) state ──────────────────────────────────────────────
|
|
2150
|
+
// All fields reset together in stopIde(). Never auto-started; only spawned
|
|
2151
|
+
// by an explicit start_ide data-channel command from the frontend.
|
|
2152
|
+
// Cloudflared is gone — exposure is via the agent's own HTTP server proxy
|
|
2153
|
+
// (ideProxyEnabled at module scope, set/cleared by start_ide/stopIde).
|
|
2154
|
+
let ideCodeServerProc = null;
|
|
2155
|
+
let ideLastActivity = 0; // bumped on code-server stdout/stderr
|
|
2156
|
+
let ideIdleWatcher = null;
|
|
2157
|
+
// In-flight start promise: set for the duration of startCodeServer()'s async
|
|
2158
|
+
// body, cleared in its finally. Concurrent callers (e.g. /editor page refreshes
|
|
2159
|
+
// every 3s) return early instead of killing the process that is still booting.
|
|
2160
|
+
let ideStartInProgress = null;
|
|
2161
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
1993
2162
|
// LIVE meeting transcript → LLM (buffered webhook finals). See recall.on('transcript').
|
|
1994
2163
|
const meetingTranscriptBuffer = [];
|
|
1995
2164
|
let meetingFlushTimer = null;
|
|
@@ -2164,6 +2333,206 @@ async function main() {
|
|
|
2164
2333
|
}
|
|
2165
2334
|
}, MEETING_MAX_MS);
|
|
2166
2335
|
};
|
|
2336
|
+
// ── IDE helpers ─────────────────────────────────────────────────────────
|
|
2337
|
+
// ensureCodeServer() — idempotent: installs code-server if missing.
|
|
2338
|
+
// Cloudflared is no longer downloaded here (exposure via agent's own HTTP proxy).
|
|
2339
|
+
// Emits ide_status:'installing' before any long download so the frontend can
|
|
2340
|
+
// show a spinner. (~36s + ~740MB first run, once per machine lifetime).
|
|
2341
|
+
const ensureCodeServer = async () => {
|
|
2342
|
+
// Resolve code-server binary
|
|
2343
|
+
const which = await new Promise((res) => {
|
|
2344
|
+
const p = spawn('which', ['code-server']);
|
|
2345
|
+
let out = '';
|
|
2346
|
+
p.stdout?.on('data', (d) => { out += d.toString(); });
|
|
2347
|
+
p.on('close', () => res(out.trim()));
|
|
2348
|
+
});
|
|
2349
|
+
if (!which) {
|
|
2350
|
+
await sendToFrontend({ type: 'ide_status', status: 'installing' });
|
|
2351
|
+
console.log('⬇️ Installing code-server via official script...');
|
|
2352
|
+
await new Promise((res, rej) => {
|
|
2353
|
+
const install = spawn('sh', ['-c', 'curl -fsSL https://code-server.dev/install.sh | sh'], {
|
|
2354
|
+
env: { ...process.env },
|
|
2355
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2356
|
+
});
|
|
2357
|
+
install.stdout?.on('data', (d) => process.stdout.write(d));
|
|
2358
|
+
install.stderr?.on('data', (d) => process.stderr.write(d));
|
|
2359
|
+
install.on('close', (code) => code === 0 ? res() : rej(new Error(`code-server install exited ${code}`)));
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
// Kill code-server, disable proxy, clear interval, reset IDE state.
|
|
2364
|
+
const stopIde = (reason) => {
|
|
2365
|
+
ideProxyEnabled = false;
|
|
2366
|
+
ideSessionToken = null;
|
|
2367
|
+
ideStartInProgress = null;
|
|
2368
|
+
if (ideIdleWatcher) {
|
|
2369
|
+
clearInterval(ideIdleWatcher);
|
|
2370
|
+
ideIdleWatcher = null;
|
|
2371
|
+
}
|
|
2372
|
+
if (ideCodeServerProc) {
|
|
2373
|
+
try {
|
|
2374
|
+
ideCodeServerProc.kill();
|
|
2375
|
+
}
|
|
2376
|
+
catch { }
|
|
2377
|
+
ideCodeServerProc = null;
|
|
2378
|
+
}
|
|
2379
|
+
ideLastActivity = 0;
|
|
2380
|
+
ideLastProxiedActivity = 0;
|
|
2381
|
+
const msg = { type: 'ide_stopped' };
|
|
2382
|
+
if (reason)
|
|
2383
|
+
msg.reason = reason;
|
|
2384
|
+
sendToFrontend(msg).catch(() => { });
|
|
2385
|
+
console.log(`🛑 IDE stopped${reason ? ` (${reason})` : ''}`);
|
|
2386
|
+
};
|
|
2387
|
+
// Idle watcher: poll every 60s; stop IDE if no proxied HTTP/WS activity
|
|
2388
|
+
// within the threshold. Uses ideLastProxiedActivity (real editor traffic seen
|
|
2389
|
+
// by the agent's proxy) as the primary signal — more reliable than log noise
|
|
2390
|
+
// on code-server's stdout/stderr. Falls back to ideLastActivity (process output)
|
|
2391
|
+
// if no proxied activity has ever been recorded (pre-first-browser-open).
|
|
2392
|
+
const startIdeIdleWatcher = () => {
|
|
2393
|
+
if (ideIdleWatcher) {
|
|
2394
|
+
clearInterval(ideIdleWatcher);
|
|
2395
|
+
ideIdleWatcher = null;
|
|
2396
|
+
}
|
|
2397
|
+
const idleMinutes = Math.max(1, parseInt(process.env.OSBORN_IDE_IDLE_MIN || '10', 10));
|
|
2398
|
+
const idleMs = idleMinutes * 60 * 1000;
|
|
2399
|
+
ideIdleWatcher = setInterval(() => {
|
|
2400
|
+
if (!ideCodeServerProc) {
|
|
2401
|
+
clearInterval(ideIdleWatcher);
|
|
2402
|
+
ideIdleWatcher = null;
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
// Prefer proxied-activity timestamp; fall back to process output timestamp.
|
|
2406
|
+
const lastSeen = ideLastProxiedActivity > 0 ? ideLastProxiedActivity : ideLastActivity;
|
|
2407
|
+
const elapsed = Date.now() - lastSeen;
|
|
2408
|
+
if (elapsed > idleMs) {
|
|
2409
|
+
console.log(`⏱️ IDE idle for ${Math.round(elapsed / 60000)} min — stopping`);
|
|
2410
|
+
stopIde('idle');
|
|
2411
|
+
}
|
|
2412
|
+
}, 60_000);
|
|
2413
|
+
};
|
|
2414
|
+
// ── startCodeServer() ───────────────────────────────────────────────────────
|
|
2415
|
+
// Shared helper called by both the start_ide data-channel handler and the
|
|
2416
|
+
// GET /editor HTTP route. Idempotent: no-op if code-server is already running
|
|
2417
|
+
// and the proxy is enabled. On fresh start it:
|
|
2418
|
+
// 1. ensureCodeServer() — installs if missing
|
|
2419
|
+
// 2. probes --idle-timeout-seconds flag support
|
|
2420
|
+
// 3. spawns code-server on 127.0.0.1:8300 with --auth none, cwd /workspace
|
|
2421
|
+
// 4. polls until ready (up to 20 s)
|
|
2422
|
+
// 5. mints ideSessionToken (the cookie marker) and sets ideProxyEnabled = true
|
|
2423
|
+
const startCodeServer = async () => {
|
|
2424
|
+
// Fast path 1: already fully up — nothing to do.
|
|
2425
|
+
if (ideCodeServerProc && ideProxyEnabled) {
|
|
2426
|
+
console.log('🖥️ IDE already running — reusing existing instance');
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
// Fast path 2: a start is already in progress — join it instead of killing
|
|
2430
|
+
// the process that is still booting (prevents the /editor 3s-refresh self-DoS).
|
|
2431
|
+
if (ideStartInProgress) {
|
|
2432
|
+
console.log('🖥️ IDE start already in progress — awaiting in-flight promise');
|
|
2433
|
+
return ideStartInProgress;
|
|
2434
|
+
}
|
|
2435
|
+
// Fresh start: kill any stale proc (shouldn't normally exist here, but be safe).
|
|
2436
|
+
if (ideCodeServerProc)
|
|
2437
|
+
stopIde();
|
|
2438
|
+
let resolveFn;
|
|
2439
|
+
let rejectFn;
|
|
2440
|
+
ideStartInProgress = new Promise((res, rej) => { resolveFn = res; rejectFn = rej; });
|
|
2441
|
+
try {
|
|
2442
|
+
await ensureCodeServer();
|
|
2443
|
+
// Probe whether this code-server version supports --idle-timeout-seconds
|
|
2444
|
+
// (landed ~Oct 2025; older installs predate it). Only pass the flag if
|
|
2445
|
+
// the binary reports it — an unknown flag prevents code-server from starting.
|
|
2446
|
+
const idleMinutes = Math.max(1, parseInt(process.env.OSBORN_IDE_IDLE_MIN || '10', 10));
|
|
2447
|
+
const idleSeconds = idleMinutes * 60;
|
|
2448
|
+
const supportsIdleFlag = await new Promise((res) => {
|
|
2449
|
+
const h = spawn('sh', ['-c', 'code-server --help 2>&1 | grep -q idle-timeout-seconds && echo yes || echo no'], {
|
|
2450
|
+
env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'],
|
|
2451
|
+
});
|
|
2452
|
+
let out = '';
|
|
2453
|
+
h.stdout?.on('data', (d) => { out += d.toString(); });
|
|
2454
|
+
h.on('close', () => res(out.trim() === 'yes'));
|
|
2455
|
+
});
|
|
2456
|
+
console.log(`🖥️ code-server idle-timeout-seconds flag: ${supportsIdleFlag ? 'supported' : 'absent (using watcher)'}`);
|
|
2457
|
+
// Spawn code-server bound to 127.0.0.1:8300 (never 8741).
|
|
2458
|
+
console.log('🖥️ Spawning code-server on 127.0.0.1:8300...');
|
|
2459
|
+
const csArgs = [
|
|
2460
|
+
'--bind-addr', '127.0.0.1:8300',
|
|
2461
|
+
'--auth', 'none',
|
|
2462
|
+
'/workspace',
|
|
2463
|
+
];
|
|
2464
|
+
if (supportsIdleFlag) {
|
|
2465
|
+
csArgs.push('--idle-timeout-seconds', String(Math.max(60, idleSeconds)));
|
|
2466
|
+
}
|
|
2467
|
+
const codeServerProc = spawn('code-server', csArgs, {
|
|
2468
|
+
env: { ...process.env },
|
|
2469
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2470
|
+
});
|
|
2471
|
+
ideCodeServerProc = codeServerProc;
|
|
2472
|
+
ideLastActivity = Date.now();
|
|
2473
|
+
ideLastProxiedActivity = 0;
|
|
2474
|
+
// Bump lastActivity on any output from code-server (fallback idle signal).
|
|
2475
|
+
codeServerProc.stdout?.on('data', () => { ideLastActivity = Date.now(); });
|
|
2476
|
+
codeServerProc.stderr?.on('data', () => { ideLastActivity = Date.now(); });
|
|
2477
|
+
codeServerProc.on('exit', (code) => {
|
|
2478
|
+
console.log(`🖥️ code-server exited (${code})`);
|
|
2479
|
+
if (ideCodeServerProc === codeServerProc)
|
|
2480
|
+
stopIde();
|
|
2481
|
+
});
|
|
2482
|
+
// Poll until code-server is ready (302 or 200 on the root, up to 20s).
|
|
2483
|
+
// Minor fix: if the process we spawned has been replaced or killed mid-poll,
|
|
2484
|
+
// reject immediately rather than burning the rest of the 20s timeout.
|
|
2485
|
+
await new Promise((res, rej) => {
|
|
2486
|
+
let attempts = 0;
|
|
2487
|
+
const poll = setInterval(() => {
|
|
2488
|
+
// Early-exit: the proc we spawned is no longer the active one (stopIde
|
|
2489
|
+
// was called, or a concurrent path replaced it).
|
|
2490
|
+
if (ideCodeServerProc !== codeServerProc) {
|
|
2491
|
+
clearInterval(poll);
|
|
2492
|
+
rej(new Error('code-server process was replaced or stopped mid-poll'));
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
attempts++;
|
|
2496
|
+
const check = spawn('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', 'http://127.0.0.1:8300']);
|
|
2497
|
+
let out = '';
|
|
2498
|
+
check.stdout?.on('data', (d) => { out += d.toString(); });
|
|
2499
|
+
check.on('close', () => {
|
|
2500
|
+
const httpCode = parseInt(out.trim(), 10);
|
|
2501
|
+
if (httpCode === 200 || httpCode === 302 || httpCode === 301) {
|
|
2502
|
+
clearInterval(poll);
|
|
2503
|
+
res();
|
|
2504
|
+
}
|
|
2505
|
+
else if (attempts >= 40) { // 40 * 500ms = 20s
|
|
2506
|
+
clearInterval(poll);
|
|
2507
|
+
rej(new Error('code-server did not become ready within 20s'));
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
}, 500);
|
|
2511
|
+
});
|
|
2512
|
+
console.log('✅ code-server ready on 8300');
|
|
2513
|
+
// Mint the session cookie token and enable the proxy. From this point the
|
|
2514
|
+
// /editor route will 302+Set-Cookie and the fall-through proxy will only
|
|
2515
|
+
// forward requests that carry the valid osborn_ide cookie.
|
|
2516
|
+
ideSessionToken = randomBytes(24).toString('hex');
|
|
2517
|
+
ideProxyEnabled = true;
|
|
2518
|
+
// Start idle watcher only when the native idle flag is absent.
|
|
2519
|
+
if (!supportsIdleFlag) {
|
|
2520
|
+
startIdeIdleWatcher();
|
|
2521
|
+
}
|
|
2522
|
+
resolveFn();
|
|
2523
|
+
}
|
|
2524
|
+
catch (err) {
|
|
2525
|
+
rejectFn(err);
|
|
2526
|
+
throw err;
|
|
2527
|
+
}
|
|
2528
|
+
finally {
|
|
2529
|
+
ideStartInProgress = null;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
// Wire the module-scope hook so startApiServer()'s /editor route can call
|
|
2533
|
+
// startCodeServer() without reaching into main()'s closure directly.
|
|
2534
|
+
codeServerStartHook = startCodeServer;
|
|
2535
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
2167
2536
|
// Track the active resume session ID across scopes (ParticipantConnected + DataReceived)
|
|
2168
2537
|
// Updated by resume_session, session_selected, continue_session, switch_session handlers
|
|
2169
2538
|
let currentResumeSessionId;
|
|
@@ -4900,6 +5269,11 @@ async function main() {
|
|
|
4900
5269
|
armMeetingLeaveGrace(); // no-op when no meeting, kept for symmetry
|
|
4901
5270
|
armFastLeaveTimer();
|
|
4902
5271
|
}
|
|
5272
|
+
// Tear down IDE if one is running — user has disconnected, no point keeping
|
|
5273
|
+
// code-server alive for a departed session.
|
|
5274
|
+
if (ideCodeServerProc) {
|
|
5275
|
+
stopIde();
|
|
5276
|
+
}
|
|
4903
5277
|
console.log('⏳ Waiting for new user...\n');
|
|
4904
5278
|
};
|
|
4905
5279
|
const handleDataReceived = async (payload, participant, kind, topic) => {
|
|
@@ -5663,6 +6037,32 @@ async function main() {
|
|
|
5663
6037
|
await sendToFrontend({ type: 'meeting_error', message: err.message });
|
|
5664
6038
|
}
|
|
5665
6039
|
}
|
|
6040
|
+
// ── IDE (code-server) handlers ──────────────────────────────────────
|
|
6041
|
+
else if (data.type === 'start_ide') {
|
|
6042
|
+
try {
|
|
6043
|
+
await startCodeServer();
|
|
6044
|
+
// Build the /editor entry-point URL for the frontend to open.
|
|
6045
|
+
const flyAppHost = process.env.FLY_APP_NAME
|
|
6046
|
+
? `${process.env.FLY_APP_NAME}.fly.dev`
|
|
6047
|
+
: (process.env.OSBORN_PUBLIC_HOST || `localhost:${apiPort}`);
|
|
6048
|
+
const ideUrl = process.env.FLY_APP_NAME
|
|
6049
|
+
? `https://${flyAppHost}/editor`
|
|
6050
|
+
: `http://${flyAppHost}/editor`;
|
|
6051
|
+
console.log(`✅ IDE proxy ready: ${ideUrl}`);
|
|
6052
|
+
await sendToFrontend({ type: 'ide_ready', url: ideUrl });
|
|
6053
|
+
}
|
|
6054
|
+
catch (err) {
|
|
6055
|
+
console.error('❌ start_ide error:', err);
|
|
6056
|
+
// Clean up any partial state.
|
|
6057
|
+
if (ideCodeServerProc)
|
|
6058
|
+
stopIde();
|
|
6059
|
+
await sendToFrontend({ type: 'ide_error', error: err.message || String(err) });
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
else if (data.type === 'stop_ide') {
|
|
6063
|
+
stopIde();
|
|
6064
|
+
}
|
|
6065
|
+
// ───────────────────────────────────────────────────────────────────
|
|
5666
6066
|
else if (data.type === 'session_selected') {
|
|
5667
6067
|
const sessionId = data.sessionId;
|
|
5668
6068
|
console.log(`🚪 Session gate completed: ${sessionId ? `resume ${sessionId}` : 'fresh start'}`);
|
|
@@ -5815,6 +6215,45 @@ async function main() {
|
|
|
5815
6215
|
slots: [...focusedEntry, ...bgEntries],
|
|
5816
6216
|
});
|
|
5817
6217
|
}
|
|
6218
|
+
else if (data.type === 'list_processes') {
|
|
6219
|
+
// Read process list from /proc. Each numeric subdir is a PID.
|
|
6220
|
+
// We read /proc/<pid>/comm (process name) and VmRSS from /proc/<pid>/status.
|
|
6221
|
+
// Skips any PID that errors mid-scan (race: process may have exited).
|
|
6222
|
+
try {
|
|
6223
|
+
const procEntries = readdirSync('/proc');
|
|
6224
|
+
const processes = [];
|
|
6225
|
+
for (const entry of procEntries) {
|
|
6226
|
+
if (!/^\d+$/.test(entry))
|
|
6227
|
+
continue;
|
|
6228
|
+
const pid = parseInt(entry, 10);
|
|
6229
|
+
try {
|
|
6230
|
+
const comm = readFileSync(`/proc/${entry}/comm`, 'utf8').trim();
|
|
6231
|
+
const status = readFileSync(`/proc/${entry}/status`, 'utf8');
|
|
6232
|
+
const vmRssMatch = status.match(/^VmRSS:\s*(\d+)\s*kB/m);
|
|
6233
|
+
const rssMb = vmRssMatch ? Math.round(parseInt(vmRssMatch[1], 10) / 1024) : 0;
|
|
6234
|
+
processes.push({ pid, name: comm, rssMb });
|
|
6235
|
+
}
|
|
6236
|
+
catch {
|
|
6237
|
+
// Process exited between readdir and read — skip it
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
// Sort by resident memory descending, cap at 40 entries
|
|
6241
|
+
processes.sort((a, b) => b.rssMb - a.rssMb);
|
|
6242
|
+
const topProcesses = processes.slice(0, 40);
|
|
6243
|
+
// Reuse the existing os module memory totals (same source as /health endpoint)
|
|
6244
|
+
const totalMb = Math.round(totalmem() / 1024 / 1024);
|
|
6245
|
+
const freeMb = Math.round(freemem() / 1024 / 1024);
|
|
6246
|
+
const usedMb = totalMb - freeMb;
|
|
6247
|
+
await sendToFrontend({
|
|
6248
|
+
type: 'process_list',
|
|
6249
|
+
processes: topProcesses,
|
|
6250
|
+
memory: { usedMb, totalMb, freeMb },
|
|
6251
|
+
});
|
|
6252
|
+
}
|
|
6253
|
+
catch (err) {
|
|
6254
|
+
console.error('❌ list_processes: failed to read /proc:', err);
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
5818
6257
|
}
|
|
5819
6258
|
catch { }
|
|
5820
6259
|
};
|