@nocoo/eagle-agent 0.3.0 → 0.5.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.
- package/README.md +41 -7
- package/dist/agent/cli.js +25 -5
- package/dist/agent/collector.js +69 -12
- package/dist/agent/manager.js +402 -0
- package/dist/agent/realtime.js +379 -0
- package/dist/agent/terminal-input.js +210 -0
- package/dist/agent/version.js +2 -1
- package/dist/package.json +50 -0
- package/dist/src/shared/realtime.js +100 -0
- package/dist/src/shared/summaries.js +114 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,24 +6,24 @@ Read-only Herdr inventory, task evidence, machine resources and named TCP port c
|
|
|
6
6
|
|
|
7
7
|
Check `node --version`, `npm --version` and `herdr --version` first. Install Node.js 24+ from https://nodejs.org/en/download if needed, and have Herdr installed and running. No Eagle repository checkout, TypeScript compiler or npm login is required to install this public package.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Install Agent v0.5.0 from npm. The Agent and Eagle website share the same release version:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install -g @nocoo/eagle-agent@0.
|
|
12
|
+
npm install -g @nocoo/eagle-agent@0.5.0 --registry=https://registry.npmjs.org
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
**If npm
|
|
15
|
+
**If npm downloads time out, use the Tencent Cloud mirror / 下载超时时首选腾讯云镜像:**
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm install -g @nocoo/eagle-agent@0.
|
|
18
|
+
npm install -g @nocoo/eagle-agent@0.5.0 --registry=https://mirrors.cloud.tencent.com/npm/
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`--registry` applies only to this installation; it does not change your global npm configuration. Mirrors may
|
|
21
|
+
`--registry` applies only to this installation; it does not change your global npm configuration. Mirrors may lag (`404` / `ETARGET`); retry later or use the official registry when reachable. Keep the pinned version, HTTPS and certificate verification. Eagle credentials are unrelated to npm and must never be sent to a registry.
|
|
22
22
|
|
|
23
23
|
Verify the installation before configuring the agent:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
eagle-agent --version # expected: 0.
|
|
26
|
+
eagle-agent --version # expected: 0.5.0
|
|
27
27
|
eagle-agent --help
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -45,8 +45,42 @@ eagle-agent heartbeat
|
|
|
45
45
|
|
|
46
46
|
`once` collects all running Herdr sessions and sends a complete snapshot. `watch` repeats every 30 seconds by default. Set up a user launchd or systemd service to keep it running across restarts, with the correct absolute executable paths and PATH for Node and Herdr. Do not run overlapping collectors for one configuration.
|
|
47
47
|
|
|
48
|
-
The machine's Durable Object maintains current state. Unacknowledged reports remain in a private local spool; retries preserve their IDs. Older reports cannot roll back current state.
|
|
48
|
+
The machine's Durable Object maintains current state. Unacknowledged reports remain in a private local spool; retries preserve their IDs. Older reports cannot roll back current state. Semantic changes are retained independently in DO UTC hourly buckets and replicated to D1; whole-report archives and hourly AI aggregation remain paused.
|
|
49
49
|
|
|
50
50
|
Optional `watchPorts`: `[{ "name": "Raven", "port": 7024 }]`. Only loopback TCP checks are supported; a listening port does not prove service or task health. CPU, RAM, disk and uptime are sampled automatically. Task status combines terminal summaries, goals, Git, tests, processes and deployment evidence; pane lifecycle badges are weak hints.
|
|
51
51
|
|
|
52
52
|
Detailed configuration and manager evidence format: https://github.com/nocoo/eagle/blob/main/docs/AGENT.md
|
|
53
|
+
|
|
54
|
+
## Continuous Pane summaries
|
|
55
|
+
|
|
56
|
+
Eagle is agent-neutral. Reuse the machine's existing management Agent; **Hermes Agent is recommended, not required**. Cherry is one machine's local Hermes profile/alias, not a product or dependency to install. Do not search for or install an unrelated Cherry package. If no suitable Agent is configured yet, keep `eagle-agent watch` running and report that semantic setup is pending.
|
|
57
|
+
|
|
58
|
+
Set `manager.command` explicitly in the existing secure config, preserving its other fields. The command is an argv array, executed without a shell: it receives a UTF-8 instruction followed by bounded input JSON on stdin and must return the requested JSON array on stdout. Use the existing model/provider/profile, disable tools and avoid interactive prompts. A small adapter script can normalize another Agent's input/output. Eagle does not install an Agent or change its model configuration.
|
|
59
|
+
|
|
60
|
+
For an already configured Hermes, inspect `hermes chat --help`, locate its executable and replace the example path below. Only use flags supported by that installed version. The official project is https://github.com/NousResearch/hermes-agent; follow its installation instructions only if Hermes is actually needed. A working alternative Agent needs no Hermes installation.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"manager": {
|
|
65
|
+
"id": "manager",
|
|
66
|
+
"command": ["/absolute/path/to/hermes", "chat", "--query-file", "-", "--oneshot", "--quiet", "--toolsets", "none", "--ignore-rules", "--source", "tool", "--max-turns", "1", "--run-budget", "55"],
|
|
67
|
+
"minIntervalSeconds": 120,
|
|
68
|
+
"batchSize": 8
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This is a config fragment, not a replacement for `agent.json`. For a named Hermes profile, use its existing launcher/wrapper or documented profile selection; retain the same provider credentials and service environment. The example does not pass model/provider overrides or `--ignore-user-config`. For another Agent, replace the entire `command` array with its verified stdin/noninteractive invocation or adapter; do not reuse Hermes flags blindly. Keep credentials out of argv and model input.
|
|
74
|
+
|
|
75
|
+
Run `eagle-agent manager-once`, verify actual summaries, then supervise `eagle-agent manager-watch` separately from `watch` using absolute paths and the required PATH/profile environment. Alternatively schedule `manager-once` with the machine's existing cron/scheduler; verify that scheduler's syntax and do not run both schedulers. A single successful cycle does not provide continuous coverage.
|
|
76
|
+
|
|
77
|
+
Manager has a 65-second subprocess deadline and a persistent 120-second minimum per Pane/task. It interprets changed inputs only; stable inputs refresh freshness without another model call or history entry. Each summary describes task, phase, progress, outcomes, blocker, next step and rationale with deterministic evidence references. Neither Agent output nor a terminal completion claim certifies tests or deployment.
|
|
78
|
+
|
|
79
|
+
Preserve the existing `manager.id` and `manager-MACHINE_ID/` directory across upgrades, even if its old ID is `cherry`: IDs are durable writer identities, not executable names. Keep sequence, pending batches and cached summaries. When upgrading from 0.4.0's implicit Cherry default, explicitly configure the already working command and keep ID `cherry`; do not reset identity or install a different Agent.
|
|
80
|
+
|
|
81
|
+
Protocol, independent DO streams, hourly history and retention: https://github.com/nocoo/eagle/blob/main/docs/PANE-SUMMARIES.md
|
|
82
|
+
Reusable Skill: https://github.com/nocoo/eagle/blob/main/skills/eagle-report/SKILL.md
|
|
83
|
+
|
|
84
|
+
## Space realtime control
|
|
85
|
+
|
|
86
|
+
Run `eagle-agent realtime-watch` as a separate user service using the same secure configuration. This enables authenticated web control through an outbound WebSocket; the collector remains read-only. Open a Space in Eagle and select 实时模式, then 接管输入. Unwatched Spaces consume no screen polling. Leaving/hiding the page releases the subscription; input is never replayed after a disconnect. Details and limits: https://github.com/nocoo/eagle/blob/main/docs/REALTIME.md
|
package/dist/agent/cli.js
CHANGED
|
@@ -5,6 +5,8 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { ReportSchema, WatchPortsSchema } from "../src/shared/schema.js";
|
|
7
7
|
import { checkUrl, collect, sendReport, } from "./collector.js";
|
|
8
|
+
import { ManagerConfigSchema, managerTick } from "./manager.js";
|
|
9
|
+
import { realtimeWatch } from "./realtime.js";
|
|
8
10
|
import { drainSpool } from "./spool.js";
|
|
9
11
|
import { AGENT_VERSION } from "./version.js";
|
|
10
12
|
const ConfigSchema = z.strictObject({
|
|
@@ -17,6 +19,7 @@ const ConfigSchema = z.strictObject({
|
|
|
17
19
|
codexDir: z.string().optional(),
|
|
18
20
|
spoolDir: z.string().optional(),
|
|
19
21
|
watchPorts: WatchPortsSchema.default([]),
|
|
22
|
+
manager: ManagerConfigSchema.optional(),
|
|
20
23
|
});
|
|
21
24
|
const path = process.env.EAGLE_CONFIG || join(homedir(), ".config/eagle/agent.json");
|
|
22
25
|
async function heartbeat(config, warning) {
|
|
@@ -74,7 +77,7 @@ async function cycle(config) {
|
|
|
74
77
|
async function main() {
|
|
75
78
|
const action = process.argv[2] || "once";
|
|
76
79
|
if (action === "--help" || action === "help") {
|
|
77
|
-
console.log("Eagle Agent\nCommands: init (JSON on stdin), collect <file>, upload <file>, once, watch, heartbeat\nConfig: EAGLE_CONFIG or ~/.config/eagle/agent.json (0600). Node.js 24+ and Herdr required.");
|
|
80
|
+
console.log("Eagle Agent\nCommands: init (JSON on stdin), collect <file>, upload <file>, once, watch, realtime-watch, heartbeat, manager-once, manager-watch\nConfig: EAGLE_CONFIG or ~/.config/eagle/agent.json (0600). Node.js 24+ and Herdr required. Manager requires explicit manager.command for your existing agent (Hermes recommended); deterministic collection is independent.");
|
|
78
81
|
return;
|
|
79
82
|
}
|
|
80
83
|
if (action === "--version") {
|
|
@@ -115,6 +118,8 @@ async function main() {
|
|
|
115
118
|
throw new Error("Config must have mode 0600");
|
|
116
119
|
const config = ConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
117
120
|
checkUrl(config.url);
|
|
121
|
+
if (action.startsWith("manager-") && !config.manager?.command)
|
|
122
|
+
throw new Error("Configure manager.command for your existing agent (Hermes recommended) before starting Manager; watch works independently.");
|
|
118
123
|
if (action === "collect") {
|
|
119
124
|
const output = process.argv[3];
|
|
120
125
|
if (!output)
|
|
@@ -133,13 +138,21 @@ async function main() {
|
|
|
133
138
|
throw new Error("Machine identity mismatch");
|
|
134
139
|
console.log(JSON.stringify(await sendReport(config.url, config.token, report)));
|
|
135
140
|
}
|
|
141
|
+
else if (action === "realtime-watch") {
|
|
142
|
+
const stop = new AbortController();
|
|
143
|
+
process.once("SIGTERM", () => stop.abort());
|
|
144
|
+
process.once("SIGINT", () => stop.abort());
|
|
145
|
+
await realtimeWatch(config, stop.signal);
|
|
146
|
+
}
|
|
136
147
|
else if (action === "heartbeat") {
|
|
137
148
|
await heartbeat(config);
|
|
138
149
|
console.log("Heartbeat accepted");
|
|
139
150
|
}
|
|
140
151
|
else if (action === "once")
|
|
141
152
|
await cycle(config);
|
|
142
|
-
else if (action === "
|
|
153
|
+
else if (action === "manager-once")
|
|
154
|
+
console.log(JSON.stringify(await managerTick(config, join(dirname(path), `manager-${config.machineId}`))));
|
|
155
|
+
else if (action === "watch" || action === "manager-watch") {
|
|
143
156
|
let running = true;
|
|
144
157
|
process.on("SIGTERM", () => {
|
|
145
158
|
running = false;
|
|
@@ -150,11 +163,18 @@ async function main() {
|
|
|
150
163
|
while (running) {
|
|
151
164
|
const start = Date.now();
|
|
152
165
|
try {
|
|
153
|
-
|
|
166
|
+
if (action === "manager-watch")
|
|
167
|
+
console.log(JSON.stringify({
|
|
168
|
+
event: "manager_reported",
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
...(await managerTick(config, join(dirname(path), `manager-${config.machineId}`))),
|
|
171
|
+
}));
|
|
172
|
+
else
|
|
173
|
+
await cycle(config);
|
|
154
174
|
}
|
|
155
175
|
catch (e) {
|
|
156
176
|
console.error(JSON.stringify({
|
|
157
|
-
event: "report_failed",
|
|
177
|
+
event: action === "manager-watch" ? "manager_failed" : "report_failed",
|
|
158
178
|
message: e instanceof z.ZodError
|
|
159
179
|
? "Invalid config or report schema"
|
|
160
180
|
: e instanceof Error
|
|
@@ -167,7 +187,7 @@ async function main() {
|
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
189
|
else
|
|
170
|
-
throw new Error("Commands: collect <file>, upload <file>, once, watch, heartbeat");
|
|
190
|
+
throw new Error("Commands: collect <file>, upload <file>, once, watch, realtime-watch, heartbeat, manager-once, manager-watch");
|
|
171
191
|
}
|
|
172
192
|
void main().catch((e) => {
|
|
173
193
|
console.error(e instanceof z.ZodError
|
package/dist/agent/collector.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, open, readFile, rename, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -12,7 +12,8 @@ import { AGENT_VERSION } from "./version.js";
|
|
|
12
12
|
const exec = promisify(execFile);
|
|
13
13
|
export function redact(text, secrets = []) {
|
|
14
14
|
let clean = stripVTControlCharacters(text)
|
|
15
|
-
.replace(/-----BEGIN [\w ]*PRIVATE KEY-----[\s\S]
|
|
15
|
+
.replace(/-----BEGIN [\w ]*PRIVATE KEY-----[\s\S]*?(?:-----END [\w ]*PRIVATE KEY-----|$)/g, "[REDACTED KEY]")
|
|
16
|
+
.replace(/(?:^[ \t]*[A-Za-z0-9+/]+={0,2}[ \t]*\r?\n)+[ \t]*-----END [\w ]*PRIVATE KEY-----/gm, "[REDACTED KEY]")
|
|
16
17
|
.replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]")
|
|
17
18
|
.replace(/\beag1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED]")
|
|
18
19
|
.replace(/((?:[A-Z_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|CREDENTIAL)[A-Z_]*)\s*[=:]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;]+)/gi, "$1[REDACTED]")
|
|
@@ -22,7 +23,7 @@ export function redact(text, secrets = []) {
|
|
|
22
23
|
clean = clean.replaceAll(secret, "[REDACTED]");
|
|
23
24
|
return clean;
|
|
24
25
|
}
|
|
25
|
-
export function normalizeSnapshot(snapshot, session) {
|
|
26
|
+
export function normalizeSnapshot(snapshot, session, secrets = []) {
|
|
26
27
|
if (!Array.isArray(snapshot.workspaces) ||
|
|
27
28
|
!Array.isArray(snapshot.tabs) ||
|
|
28
29
|
!Array.isArray(snapshot.panes) ||
|
|
@@ -44,9 +45,9 @@ export function normalizeSnapshot(snapshot, session) {
|
|
|
44
45
|
panes: rawPanes.map((p, index) => {
|
|
45
46
|
const rect = layout?.panes.find((r) => r.pane_id === p.pane_id)?.rect;
|
|
46
47
|
const area = layout?.area;
|
|
47
|
-
const title = (p.title ||
|
|
48
|
+
const title = redact(p.title ||
|
|
48
49
|
p.terminal_title_stripped ||
|
|
49
|
-
`${p.agent || "终端"} · ${w.label}
|
|
50
|
+
`${p.agent || "终端"} · ${w.label}`, secrets).slice(0, 240);
|
|
50
51
|
return {
|
|
51
52
|
id: p.pane_id,
|
|
52
53
|
title,
|
|
@@ -56,7 +57,7 @@ export function normalizeSnapshot(snapshot, session) {
|
|
|
56
57
|
: "unknown",
|
|
57
58
|
task: {
|
|
58
59
|
id: createHash("sha256")
|
|
59
|
-
.update(`${session}:${p.pane_id}:${p.
|
|
60
|
+
.update(`${session}:${p.pane_id}:${p.agent || "shell"}:${p.agent_session?.agent && p.agent_session.agent !== p.agent ? (p.terminal_id ?? "") : (p.agent_session?.value ?? p.terminal_id ?? "")}`)
|
|
60
61
|
.digest("hex")
|
|
61
62
|
.slice(0, 32),
|
|
62
63
|
title,
|
|
@@ -202,8 +203,12 @@ const ManagerSchema = z.record(z.string(), z.strictObject({
|
|
|
202
203
|
export function applyManager(pane, managed) {
|
|
203
204
|
if (managed.task.id !== pane.task.id)
|
|
204
205
|
return false;
|
|
205
|
-
pane.task = managed.task;
|
|
206
|
-
pane.evidence.push(...managed.evidence)
|
|
206
|
+
pane.task.title = managed.task.title;
|
|
207
|
+
pane.evidence.push(...managed.evidence.map((e) => ({
|
|
208
|
+
...e,
|
|
209
|
+
status: "unknown",
|
|
210
|
+
source: `manager:legacy:${e.source}`.slice(0, 240),
|
|
211
|
+
})));
|
|
207
212
|
return true;
|
|
208
213
|
}
|
|
209
214
|
export function preserveStopped(current, previous, stopped) {
|
|
@@ -234,6 +239,51 @@ export function conversationTaskId(text, sessionId) {
|
|
|
234
239
|
.slice(0, 32)
|
|
235
240
|
: undefined;
|
|
236
241
|
}
|
|
242
|
+
export function conversationEvidence(text, agent, taskId, modifiedAt) {
|
|
243
|
+
let final;
|
|
244
|
+
for (const line of text.split("\n")) {
|
|
245
|
+
try {
|
|
246
|
+
const event = JSON.parse(line);
|
|
247
|
+
const message = event.type === "message" ? event.message : event;
|
|
248
|
+
if ((event.type === "user" && !event.synthetic_reason) ||
|
|
249
|
+
(event.type === "message" && message.role === "user"))
|
|
250
|
+
final = undefined;
|
|
251
|
+
if (!(event.type === "assistant" || message?.role === "assistant"))
|
|
252
|
+
continue;
|
|
253
|
+
if (message.tool_calls?.length ||
|
|
254
|
+
(message.stopReason && message.stopReason !== "stop"))
|
|
255
|
+
continue;
|
|
256
|
+
if (Array.isArray(message.content) &&
|
|
257
|
+
message.content.some((c) => c.type === "toolCall" || c.type === "tool_use"))
|
|
258
|
+
continue;
|
|
259
|
+
const content = typeof message.content === "string"
|
|
260
|
+
? message.content
|
|
261
|
+
: Array.isArray(message.content)
|
|
262
|
+
? message.content
|
|
263
|
+
.filter((c) => c.type === "text")
|
|
264
|
+
.map((c) => c.text)
|
|
265
|
+
.join("\n")
|
|
266
|
+
: "";
|
|
267
|
+
if (!content.trim())
|
|
268
|
+
continue;
|
|
269
|
+
const timestamp = Date.parse(event.timestamp ?? message.timestamp);
|
|
270
|
+
final = {
|
|
271
|
+
kind: "summary",
|
|
272
|
+
status: "unknown",
|
|
273
|
+
summary: content.slice(-2000),
|
|
274
|
+
source: `${agent}:final-message`,
|
|
275
|
+
taskId,
|
|
276
|
+
observedAt: Number.isFinite(timestamp)
|
|
277
|
+
? new Date(timestamp).toISOString()
|
|
278
|
+
: modifiedAt,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
/* A bounded native transcript can begin mid-line. */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return final ? [final] : [];
|
|
286
|
+
}
|
|
237
287
|
export async function collect(config) {
|
|
238
288
|
const capturedAt = new Date().toISOString();
|
|
239
289
|
const warnings = [];
|
|
@@ -284,12 +334,14 @@ export async function collect(config) {
|
|
|
284
334
|
// Any session failure aborts the whole snapshot, so a partial inventory never closes Spaces.
|
|
285
335
|
const raw = JSON.parse(await herdr(["api", "snapshot"], session.name))
|
|
286
336
|
.result.snapshot;
|
|
287
|
-
const normalized = normalizeSnapshot(raw, session.name);
|
|
337
|
+
const normalized = normalizeSnapshot(raw, session.name, [config.token]);
|
|
288
338
|
for (const space of normalized) {
|
|
289
339
|
for (const pane of space.tabs.flatMap((t) => t.panes)) {
|
|
290
340
|
const original = raw.panes.find((p) => p.pane_id === pane.id);
|
|
291
341
|
const managed = manager[`${session.name}:${pane.id}`];
|
|
292
|
-
const sessionId = original?.agent_session?.kind === "id"
|
|
342
|
+
const sessionId = original?.agent_session?.kind === "id" &&
|
|
343
|
+
(!original.agent_session.agent ||
|
|
344
|
+
original.agent_session.agent === pane.agent)
|
|
293
345
|
? original.agent_session.value
|
|
294
346
|
: undefined;
|
|
295
347
|
if (pane.agent === "codex" && sessionId) {
|
|
@@ -334,7 +386,10 @@ export async function collect(config) {
|
|
|
334
386
|
}
|
|
335
387
|
if (pane.agent === "grok" || pane.agent === "pi") {
|
|
336
388
|
try {
|
|
337
|
-
const identity = original?.agent_session?.
|
|
389
|
+
const identity = original?.agent_session?.agent &&
|
|
390
|
+
original.agent_session.agent !== pane.agent
|
|
391
|
+
? undefined
|
|
392
|
+
: original?.agent_session?.value;
|
|
338
393
|
const cwd = original?.cwd;
|
|
339
394
|
const path = pane.agent === "pi"
|
|
340
395
|
? identity
|
|
@@ -342,9 +397,11 @@ export async function collect(config) {
|
|
|
342
397
|
? join(homedir(), ".grok/sessions", encodeURIComponent(cwd), identity, "chat_history.jsonl")
|
|
343
398
|
: undefined;
|
|
344
399
|
if (path && identity) {
|
|
345
|
-
const
|
|
400
|
+
const transcript = await tail(path);
|
|
401
|
+
const taskId = conversationTaskId(transcript, identity);
|
|
346
402
|
if (taskId)
|
|
347
403
|
pane.task.id = taskId;
|
|
404
|
+
pane.evidence.push(...conversationEvidence(transcript, pane.agent, pane.task.id, (await stat(path)).mtime.toISOString()));
|
|
348
405
|
}
|
|
349
406
|
}
|
|
350
407
|
catch {
|