@prevalentware/opencode-goal-plugin 0.1.15 → 0.1.17
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 +32 -5
- package/dist/server.js +111 -9
- package/package.json +10 -2
- package/src/tui.tsx +15 -1
package/README.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# OpenCode Goal Plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@prevalentware/opencode-goal-plugin)
|
|
4
|
+
[](https://github.com/prevalentWare/opencode-goal-plugin)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
OpenCode Goal Plugin adds Codex-style long-running goal mode to OpenCode. It gives AI coding agents a `/goal` slash command, persistent goal state, completion evidence, idle continuation, and a terminal UI goal indicator so an OpenCode session can keep working toward one explicit objective until it is complete, blocked, or cleared.
|
|
8
|
+
|
|
9
|
+
If you are searching for an OpenCode goal plugin, goal mode for OpenCode, or a way to keep an OpenCode AI coding agent focused on a long-running task, this package is the npm plugin for that workflow.
|
|
10
|
+
|
|
11
|
+
Links:
|
|
12
|
+
|
|
13
|
+
- npm package: [`@prevalentware/opencode-goal-plugin`](https://www.npmjs.com/package/@prevalentware/opencode-goal-plugin)
|
|
14
|
+
- GitHub repository: [`prevalentWare/opencode-goal-plugin`](https://github.com/prevalentWare/opencode-goal-plugin)
|
|
15
|
+
- OpenCode plugin command: `opencode plugin @prevalentware/opencode-goal-plugin`
|
|
16
|
+
|
|
17
|
+
The OpenCode Goal Plugin adds:
|
|
6
18
|
|
|
7
19
|
- `/goal <objective>` as an OpenCode command for TUI, desktop, and web.
|
|
8
20
|
- A sidebar goal indicator with status, elapsed time, and objective.
|
|
@@ -12,6 +24,17 @@ This plugin adds:
|
|
|
12
24
|
- Optional automatic continuation on `session.idle`.
|
|
13
25
|
- Compaction context so active goals are preserved when OpenCode summarizes a long session.
|
|
14
26
|
|
|
27
|
+
## Why Use This OpenCode Goal Plugin?
|
|
28
|
+
|
|
29
|
+
Use this plugin when you want OpenCode to behave more like a goal-driven coding agent instead of a one-prompt assistant. A goal stays visible, survives session compaction, can continue automatically when the session becomes idle, and can only be closed with explicit evidence or a concrete blocker.
|
|
30
|
+
|
|
31
|
+
Common use cases:
|
|
32
|
+
|
|
33
|
+
- Keep an OpenCode agent focused during long refactors, migrations, reviews, or test-fixing sessions.
|
|
34
|
+
- Track one explicit objective across TUI, desktop, and web OpenCode surfaces.
|
|
35
|
+
- Require completion evidence before a goal is marked done.
|
|
36
|
+
- Preserve the current goal when OpenCode summarizes or compacts a long conversation.
|
|
37
|
+
|
|
15
38
|
## Install
|
|
16
39
|
|
|
17
40
|
Install locally for the current OpenCode project:
|
|
@@ -60,7 +83,8 @@ Server options can be configured in `opencode.json`:
|
|
|
60
83
|
{
|
|
61
84
|
"auto_continue": true,
|
|
62
85
|
"max_auto_turns": 25,
|
|
63
|
-
"min_continue_interval_seconds": 3
|
|
86
|
+
"min_continue_interval_seconds": 3,
|
|
87
|
+
"max_prompt_failures": 3
|
|
64
88
|
}
|
|
65
89
|
]
|
|
66
90
|
]
|
|
@@ -72,6 +96,7 @@ Defaults:
|
|
|
72
96
|
- `auto_continue`: `true`
|
|
73
97
|
- `max_auto_turns`: `25`
|
|
74
98
|
- `min_continue_interval_seconds`: `3`
|
|
99
|
+
- `max_prompt_failures`: `3`
|
|
75
100
|
- `register_command`: `true`
|
|
76
101
|
- `command_name`: `"goal"`
|
|
77
102
|
|
|
@@ -83,7 +108,7 @@ Use `/goal <objective>` in a fresh OpenCode chat to create a long-running goal:
|
|
|
83
108
|
/goal review the frontend and translate visible English UI text to Spanish
|
|
84
109
|
```
|
|
85
110
|
|
|
86
|
-
Bare `/goal` reports the current goal state. `/goal clear` clears the goal. The TUI also includes a `Goal` command-palette entry for viewing, refreshing, or clearing the current goal state without creating a new goal.
|
|
111
|
+
Bare `/goal` reports the current goal state. `/goal pause` pauses the goal without clearing it, and `/goal resume` resumes it. `/goal clear` clears the goal; `/goal stop`, `/goal off`, `/goal reset`, `/goal none`, and `/goal cancel` are clear aliases. The TUI also includes a `Goal` command-palette entry for viewing, refreshing, or clearing the current goal state without creating a new goal.
|
|
87
112
|
|
|
88
113
|
You can also ask the agent to formulate the objective and call `set_goal` itself, for example: "set your own goal to finish this refactor safely." The tool uses the agent-written objective but still only creates a goal when explicitly requested.
|
|
89
114
|
|
|
@@ -147,4 +172,6 @@ OpenCode plugin modules are target-specific. This package exports separate modul
|
|
|
147
172
|
}
|
|
148
173
|
```
|
|
149
174
|
|
|
150
|
-
Codex goal mode has deeper runtime integration for thread lifecycle control. This plugin implements the same workflow using OpenCode plugin hooks. Token usage is read from OpenCode step-finish usage when available and falls back to message token metadata or text estimation when exact usage is unavailable. Continuation is driven by OpenCode
|
|
175
|
+
Codex goal mode has deeper runtime integration for thread lifecycle control. This plugin implements the same workflow using OpenCode plugin hooks. Token usage is read from OpenCode step-finish usage when available and falls back to message token metadata or text estimation when exact usage is unavailable. Continuation is driven by OpenCode idle events, including `session.idle` and `session.status` idle notifications.
|
|
176
|
+
|
|
177
|
+
The goal sidebar shows the current status, elapsed time, auto-continue count, latest status message, and objective when a goal is active or paused. Closed goals remain visible briefly through the latest tool state as achieved or unmet.
|
package/dist/server.js
CHANGED
|
@@ -32,7 +32,9 @@ var GoalSchema = Schema.Struct({
|
|
|
32
32
|
closedAt: Schema.optionalWith(NullableNumber, { default: () => null }),
|
|
33
33
|
lastAccountedAt: NullableNumber,
|
|
34
34
|
autoTurns: Schema.Number,
|
|
35
|
-
lastContinuationAt: NullableNumber
|
|
35
|
+
lastContinuationAt: NullableNumber,
|
|
36
|
+
continuationFailures: Schema.optionalWith(Schema.Number, { default: () => 0 }),
|
|
37
|
+
lastStatus: Schema.optionalWith(NullableString, { default: () => null })
|
|
36
38
|
});
|
|
37
39
|
var StateSchema = Schema.Struct({
|
|
38
40
|
version: Schema.Literal(1),
|
|
@@ -145,6 +147,10 @@ function snapshot(goal) {
|
|
|
145
147
|
completionEvidence: goal.completionEvidence ?? null,
|
|
146
148
|
blocker: goal.blocker ?? null,
|
|
147
149
|
closedAt: goal.closedAt ?? null,
|
|
150
|
+
continuationFailures: goal.continuationFailures,
|
|
151
|
+
lastStatus: goal.lastStatus,
|
|
152
|
+
autoTurns: goal.autoTurns,
|
|
153
|
+
lastContinuationAt: goal.lastContinuationAt,
|
|
148
154
|
remainingTokens: null,
|
|
149
155
|
sampledAt
|
|
150
156
|
};
|
|
@@ -176,12 +182,28 @@ async function createGoal(sessionID, objective, _tokenBudget) {
|
|
|
176
182
|
closedAt: null,
|
|
177
183
|
lastAccountedAt: now,
|
|
178
184
|
autoTurns: 0,
|
|
179
|
-
lastContinuationAt: null
|
|
185
|
+
lastContinuationAt: null,
|
|
186
|
+
continuationFailures: 0,
|
|
187
|
+
lastStatus: "Goal set."
|
|
180
188
|
};
|
|
181
189
|
state.goals[sessionID] = goal;
|
|
182
190
|
return snapshot(goal);
|
|
183
191
|
});
|
|
184
192
|
}
|
|
193
|
+
async function setGoalStatus(sessionID, status) {
|
|
194
|
+
return mutate((state) => {
|
|
195
|
+
const goal = state.goals[sessionID];
|
|
196
|
+
if (!goal)
|
|
197
|
+
throw new Error("cannot update goal because this session has no goal");
|
|
198
|
+
accountWallClock(goal);
|
|
199
|
+
goal.status = status;
|
|
200
|
+
goal.updatedAt = nowSeconds();
|
|
201
|
+
goal.lastAccountedAt = status === "active" ? goal.updatedAt : null;
|
|
202
|
+
goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures;
|
|
203
|
+
goal.lastStatus = status === "active" ? "Goal resumed." : "Goal paused.";
|
|
204
|
+
return snapshot(goal);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
185
207
|
async function closeGoal(sessionID, input) {
|
|
186
208
|
return mutate((state) => {
|
|
187
209
|
const goal = state.goals[sessionID];
|
|
@@ -252,10 +274,35 @@ async function reserveContinuation(sessionID, maxAutoTurns, minIntervalSeconds)
|
|
|
252
274
|
accountWallClock(goal, now);
|
|
253
275
|
goal.autoTurns += 1;
|
|
254
276
|
goal.lastContinuationAt = now;
|
|
277
|
+
goal.lastStatus = `Auto-continue ${goal.autoTurns} reserved.`;
|
|
255
278
|
goal.updatedAt = now;
|
|
256
279
|
return snapshot(goal);
|
|
257
280
|
});
|
|
258
281
|
}
|
|
282
|
+
async function recordContinuationResult(sessionID, result, maxFailures) {
|
|
283
|
+
return mutate((state) => {
|
|
284
|
+
const goal = state.goals[sessionID];
|
|
285
|
+
if (!goal || goal.status !== "active")
|
|
286
|
+
return goal ? snapshot(goal) : null;
|
|
287
|
+
const now = nowSeconds();
|
|
288
|
+
goal.updatedAt = now;
|
|
289
|
+
if (result === "success") {
|
|
290
|
+
goal.continuationFailures = 0;
|
|
291
|
+
goal.lastStatus = "Auto-continue prompt sent.";
|
|
292
|
+
return snapshot(goal);
|
|
293
|
+
}
|
|
294
|
+
goal.continuationFailures += 1;
|
|
295
|
+
goal.lastStatus = `Auto-continue failed ${goal.continuationFailures} time(s).`;
|
|
296
|
+
if (goal.continuationFailures >= maxFailures) {
|
|
297
|
+
accountWallClock(goal, now);
|
|
298
|
+
goal.status = "paused";
|
|
299
|
+
goal.lastAccountedAt = null;
|
|
300
|
+
goal.lastStatus = `Paused after ${goal.continuationFailures} auto-continue failure(s).`;
|
|
301
|
+
goal.blocker = "Auto-continue prompt failed repeatedly. Resume the goal to retry.";
|
|
302
|
+
}
|
|
303
|
+
return snapshot(goal);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
259
306
|
function accountWallClock(goal, now = nowSeconds()) {
|
|
260
307
|
if (goal.status !== "active")
|
|
261
308
|
return;
|
|
@@ -275,8 +322,11 @@ function formatGoal(goal) {
|
|
|
275
322
|
const lines = [
|
|
276
323
|
`Objective: ${goal.objective}`,
|
|
277
324
|
`Status: ${goal.status}`,
|
|
278
|
-
`Time used: ${goal.timeUsedSeconds}s
|
|
325
|
+
`Time used: ${goal.timeUsedSeconds}s`,
|
|
326
|
+
`Auto-continues: ${goal.autoTurns}`
|
|
279
327
|
];
|
|
328
|
+
if (goal.lastStatus)
|
|
329
|
+
lines.push(`Last status: ${goal.lastStatus}`);
|
|
280
330
|
if (goal.completionEvidence)
|
|
281
331
|
lines.push(`Completion evidence: ${goal.completionEvidence}`);
|
|
282
332
|
if (goal.blocker)
|
|
@@ -335,7 +385,9 @@ Preserve the goal objective, status, elapsed time, and any completion evidence o
|
|
|
335
385
|
// src/server.ts
|
|
336
386
|
var DEFAULT_MAX_AUTO_TURNS = 25;
|
|
337
387
|
var DEFAULT_CONTINUE_INTERVAL_SECONDS = 3;
|
|
388
|
+
var DEFAULT_MAX_PROMPT_FAILURES = 3;
|
|
338
389
|
var DEFAULT_COMMAND_NAME = "goal";
|
|
390
|
+
var activeContinuations = new Set;
|
|
339
391
|
function goalCommandTemplate(commandName) {
|
|
340
392
|
return `OpenCode goal mode command "/${commandName}" was invoked.
|
|
341
393
|
|
|
@@ -348,7 +400,9 @@ Use the goal tools to handle this command:
|
|
|
348
400
|
|
|
349
401
|
- If the arguments are empty, call get_goal and briefly report the current goal state.
|
|
350
402
|
- If the arguments are "status", "show", or "current", call get_goal and briefly report the current goal state.
|
|
351
|
-
- If the arguments are "clear", call clear_goal and report whether a goal was cleared.
|
|
403
|
+
- If the arguments are "clear", "stop", "off", "reset", "none", or "cancel", call clear_goal and report whether a goal was cleared.
|
|
404
|
+
- If the arguments are "pause", pause the current goal by calling update_goal_status with status "paused" and report the result.
|
|
405
|
+
- If the arguments are "resume", resume the current goal by calling update_goal_status with status "active" and continue working toward it.
|
|
352
406
|
- If the arguments start with "complete " or "done ", perform a completion audit against real artifacts and command output. Call update_goal with status "complete" only if the goal is achieved, using concise evidence from the audit.
|
|
353
407
|
- If the arguments start with "unmet ", "blocked ", or "blocker ", call update_goal with status "unmet" only when the goal cannot be achieved or needs external input, using the remaining arguments as the blocker.
|
|
354
408
|
- Otherwise, create a new goal with create_goal. Use the full arguments as the objective.
|
|
@@ -426,10 +480,27 @@ async function sendContinuation(client, sessionID, prompt) {
|
|
|
426
480
|
}
|
|
427
481
|
});
|
|
428
482
|
}
|
|
483
|
+
function isIdleEvent(event) {
|
|
484
|
+
if (event.type === "session.idle")
|
|
485
|
+
return true;
|
|
486
|
+
const status = event.properties?.status;
|
|
487
|
+
return event.type === "session.status" && typeof status === "object" && status !== null && status.type === "idle";
|
|
488
|
+
}
|
|
489
|
+
function sessionIDFromEvent(event) {
|
|
490
|
+
const direct = event.properties?.sessionID;
|
|
491
|
+
if (typeof direct === "string")
|
|
492
|
+
return direct;
|
|
493
|
+
const info = event.properties?.info;
|
|
494
|
+
if (typeof info === "object" && info !== null && typeof info.sessionID === "string") {
|
|
495
|
+
return info.sessionID;
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
429
499
|
var server = async ({ client }, options) => {
|
|
430
500
|
const autoContinue = options?.auto_continue ?? true;
|
|
431
501
|
const maxAutoTurns = options?.max_auto_turns ?? DEFAULT_MAX_AUTO_TURNS;
|
|
432
502
|
const minInterval = options?.min_continue_interval_seconds ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
503
|
+
const maxPromptFailures = options?.max_prompt_failures ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
433
504
|
const registerCommand = options?.register_command ?? true;
|
|
434
505
|
const commandName = commandNameFromOptions(options);
|
|
435
506
|
return {
|
|
@@ -487,6 +558,17 @@ var server = async ({ client }, options) => {
|
|
|
487
558
|
return JSON.stringify({ goal, unmet_report: report }, null, 2);
|
|
488
559
|
}
|
|
489
560
|
},
|
|
561
|
+
update_goal_status: {
|
|
562
|
+
description: "Pause or resume the current OpenCode goal when the user explicitly asks to pause or resume it.",
|
|
563
|
+
args: {
|
|
564
|
+
status: z.enum(["active", "paused"]).describe("active resumes a goal; paused pauses it without clearing it.")
|
|
565
|
+
},
|
|
566
|
+
async execute(args, context) {
|
|
567
|
+
const input = args;
|
|
568
|
+
const goal = await setGoalStatus(context.sessionID, input.status);
|
|
569
|
+
return JSON.stringify({ goal }, null, 2);
|
|
570
|
+
}
|
|
571
|
+
},
|
|
490
572
|
clear_goal: {
|
|
491
573
|
description: "Clear the current OpenCode goal for this session when the user explicitly asks to clear it.",
|
|
492
574
|
args: {},
|
|
@@ -513,13 +595,33 @@ var server = async ({ client }, options) => {
|
|
|
513
595
|
output.context.push(compactionContext(goal));
|
|
514
596
|
},
|
|
515
597
|
async event({ event }) {
|
|
516
|
-
if (!autoContinue || event
|
|
598
|
+
if (!autoContinue || !isIdleEvent(event))
|
|
517
599
|
return;
|
|
518
|
-
const sessionID = event
|
|
519
|
-
|
|
520
|
-
|
|
600
|
+
const sessionID = sessionIDFromEvent(event);
|
|
601
|
+
if (!sessionID)
|
|
602
|
+
return;
|
|
603
|
+
if (activeContinuations.has(sessionID))
|
|
521
604
|
return;
|
|
522
|
-
|
|
605
|
+
activeContinuations.add(sessionID);
|
|
606
|
+
try {
|
|
607
|
+
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
608
|
+
if (!goal)
|
|
609
|
+
return;
|
|
610
|
+
await sendContinuation(client, sessionID, continuationPrompt(goal));
|
|
611
|
+
await recordContinuationResult(sessionID, "success", maxPromptFailures);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
614
|
+
await client.app?.log?.({
|
|
615
|
+
body: {
|
|
616
|
+
service: "opencode-goal-plugin",
|
|
617
|
+
level: "error",
|
|
618
|
+
message: "Auto-continue failed",
|
|
619
|
+
extra: { error: error instanceof Error ? error.message : String(error) }
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
} finally {
|
|
623
|
+
activeContinuations.delete(sessionID);
|
|
624
|
+
}
|
|
523
625
|
}
|
|
524
626
|
};
|
|
525
627
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Codex-style long-running goal mode for
|
|
3
|
+
"version": "0.1.17",
|
|
4
|
+
"description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
7
7
|
"opencode-plugin",
|
|
8
|
+
"opencode goal plugin",
|
|
9
|
+
"opencode goal mode",
|
|
10
|
+
"opencode ai",
|
|
8
11
|
"goal",
|
|
12
|
+
"goal-mode",
|
|
13
|
+
"slash-command",
|
|
14
|
+
"coding-agent",
|
|
15
|
+
"ai-agent",
|
|
16
|
+
"developer-tools",
|
|
9
17
|
"agent",
|
|
10
18
|
"tui"
|
|
11
19
|
],
|
package/src/tui.tsx
CHANGED
|
@@ -14,6 +14,10 @@ type GoalSnapshot = {
|
|
|
14
14
|
completionEvidence?: string | null
|
|
15
15
|
blocker?: string | null
|
|
16
16
|
closedAt?: number | null
|
|
17
|
+
continuationFailures: number
|
|
18
|
+
lastStatus: string | null
|
|
19
|
+
autoTurns: number
|
|
20
|
+
lastContinuationAt: number | null
|
|
17
21
|
remainingTokens: number | null
|
|
18
22
|
sampledAt?: number
|
|
19
23
|
}
|
|
@@ -155,6 +159,10 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
155
159
|
if (value.completionEvidence != null && typeof value.completionEvidence !== "string") return false
|
|
156
160
|
if (value.blocker != null && typeof value.blocker !== "string") return false
|
|
157
161
|
if (value.closedAt != null && typeof value.closedAt !== "number") return false
|
|
162
|
+
if (typeof value.continuationFailures !== "number") return false
|
|
163
|
+
if (value.lastStatus != null && typeof value.lastStatus !== "string") return false
|
|
164
|
+
if (typeof value.autoTurns !== "number") return false
|
|
165
|
+
if (value.lastContinuationAt != null && typeof value.lastContinuationAt !== "number") return false
|
|
158
166
|
if (value.remainingTokens !== null && typeof value.remainingTokens !== "number") return false
|
|
159
167
|
if (value.sampledAt != null && typeof value.sampledAt !== "number") return false
|
|
160
168
|
return true
|
|
@@ -162,7 +170,7 @@ function isGoalSnapshot(value: unknown): value is GoalSnapshot {
|
|
|
162
170
|
|
|
163
171
|
function parseGoalToolOutput(part: GoalToolPart): GoalSnapshot | null | undefined {
|
|
164
172
|
if (part.type !== "tool") return undefined
|
|
165
|
-
if (!["get_goal", "create_goal", "update_goal", "clear_goal"].includes(part.tool ?? "")) return undefined
|
|
173
|
+
if (!["get_goal", "create_goal", "update_goal", "update_goal_status", "clear_goal"].includes(part.tool ?? "")) return undefined
|
|
166
174
|
if (part.state?.status !== "completed") return undefined
|
|
167
175
|
if (part.tool === "clear_goal") return null
|
|
168
176
|
if (typeof part.state.output !== "string") return undefined
|
|
@@ -209,7 +217,9 @@ function formatGoal(goal: GoalSnapshot | null) {
|
|
|
209
217
|
`Objective: ${goal.objective}`,
|
|
210
218
|
`Status: ${visibleStatus(goal.status)}`,
|
|
211
219
|
`Time used: ${formatDuration(goal.timeUsedSeconds)}`,
|
|
220
|
+
`Auto-continues: ${goal.autoTurns}`,
|
|
212
221
|
]
|
|
222
|
+
if (goal.lastStatus) lines.push(`Last status: ${goal.lastStatus}`)
|
|
213
223
|
if (goal.completionEvidence) lines.push(`Completion evidence: ${goal.completionEvidence}`)
|
|
214
224
|
if (goal.blocker) lines.push(`Blocker: ${goal.blocker}`)
|
|
215
225
|
return lines.join("\n")
|
|
@@ -243,6 +253,10 @@ function GoalSidebar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
243
253
|
</text>
|
|
244
254
|
<text fg={theme().textMuted}>Status: {visibleStatus(value().status)}</text>
|
|
245
255
|
<text fg={theme().textMuted}>Time: {formatDuration(elapsed())}</text>
|
|
256
|
+
<text fg={theme().textMuted}>Auto-continues: {value().autoTurns}</text>
|
|
257
|
+
<Show when={value().lastStatus}>
|
|
258
|
+
{(status: () => string) => <text fg={theme().textMuted}>{status()}</text>}
|
|
259
|
+
</Show>
|
|
246
260
|
<text fg={theme().textMuted}>{objective()}</text>
|
|
247
261
|
</box>
|
|
248
262
|
}
|