@fungi.computer/corral 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +107 -2
- package/dist/index.d.ts +29 -0
- package/dist/index.js +106 -0
- package/docs/api/README.md +68 -0
- package/package.json +59 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamstack Consulting LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Corral (`@fungi.computer/corral`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Corral reports a Shiitake client's current activity in the active Herdr pane. It
|
|
4
|
+
is an optional client extension: the client supplies activity, and Herdr owns
|
|
5
|
+
the pane and its lifecycle.
|
|
6
|
+
|
|
7
|
+
## Install or set up
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @fungi.computer/corral
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The package ships ESM only and depends on `effect` and
|
|
14
|
+
[Shiitake](../shiitake/README.md) at runtime. Its only entrypoint,
|
|
15
|
+
`@fungi.computer/corral`, exports the Shiitake client extension as its default
|
|
16
|
+
export and `attachCorral` for hosts that supply their own activity stream.
|
|
17
|
+
|
|
18
|
+
The default extension reads `HERDR_ENV`, `HERDR_SOCKET_PATH`, and
|
|
19
|
+
`HERDR_PANE_ID` from its process environment. It attaches only when `HERDR_ENV`
|
|
20
|
+
is `1` and both the socket path and pane ID are set. Outside that environment,
|
|
21
|
+
attachment has no effect.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
This illustrative example attaches a stream supplied by the client. Closing the
|
|
26
|
+
Effect scope stops reporting and releases the pane's report:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import * as Effect from "effect/Effect";
|
|
30
|
+
import * as Exit from "effect/Exit";
|
|
31
|
+
import * as Scope from "effect/Scope";
|
|
32
|
+
import * as Stream from "effect/Stream";
|
|
33
|
+
import { attachCorral } from "@fungi.computer/corral";
|
|
34
|
+
|
|
35
|
+
const scope = await Effect.runPromise(Scope.make());
|
|
36
|
+
await Effect.runPromise(
|
|
37
|
+
attachCorral(Stream.fromIterable([{ state: "idle" }, { state: "working" }]), {
|
|
38
|
+
environment: {
|
|
39
|
+
HERDR_ENV: "1",
|
|
40
|
+
HERDR_SOCKET_PATH: "/path/to/herdr.sock",
|
|
41
|
+
HERDR_PANE_ID: "pane-id",
|
|
42
|
+
},
|
|
43
|
+
onError: (error) => console.error("Herdr report failed", error),
|
|
44
|
+
}).pipe(Scope.provide(scope)),
|
|
45
|
+
);
|
|
46
|
+
await Effect.runPromise(Scope.close(scope, Exit.void));
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The snippet is illustrative: a real client supplies its live activity stream and
|
|
50
|
+
the Herdr values from its managed environment. The integration tests run report
|
|
51
|
+
and release requests against a local socket server in
|
|
52
|
+
[`test/activity.test.ts`](test/activity.test.ts).
|
|
53
|
+
|
|
54
|
+
## Core concepts
|
|
55
|
+
|
|
56
|
+
The activity stream contains `idle`, `working`, `blocked`, or `unknown`. Corral
|
|
57
|
+
reports changes from that stream. It does not open another Session watch or read
|
|
58
|
+
a Session database. The client remains responsible for producing activity. The
|
|
59
|
+
default entry borrows an AsyncIterable with an independent sliding latest-value
|
|
60
|
+
subscription per iterator and a detach signal. `attachCorral` accepts an Effect
|
|
61
|
+
stream, including a stream with typed `Error` failures. Source and socket errors
|
|
62
|
+
reach `onError`; closing the scope still releases the pane report. Fungi borrows
|
|
63
|
+
its existing Session observation and command interaction store. An open agent
|
|
64
|
+
question, a pending command question or a browser hand-off reports `blocked`.
|
|
65
|
+
Answering it restores the current activity. An agent question may add its short
|
|
66
|
+
header to the blocked message; question bodies, answer choices, answers and
|
|
67
|
+
credentials are never included in reports.
|
|
68
|
+
|
|
69
|
+
Herdr derives `done` when an unseen background turn returns to `idle`. It owns
|
|
70
|
+
whether the user has seen that completion. Corral never guesses completion from
|
|
71
|
+
an unavailable observation.
|
|
72
|
+
|
|
73
|
+
`attachCorral` needs an Effect scope. When that scope closes, Corral cancels its
|
|
74
|
+
reporter, closes any in-flight socket, and releases the current pane report. It
|
|
75
|
+
sends `pane.report_agent` and `pane.release_agent` over Herdr's API socket with
|
|
76
|
+
the `fungi:agent:v1` source and `fungi` agent label. A monotone timestamp
|
|
77
|
+
sequence orders reports across attachments and process restarts.
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
See the generated [Corral API reference](docs/api/README.md). The default export
|
|
82
|
+
is an attach-only `shiitake.client/v1` entry. `attachCorral` is the lower-level
|
|
83
|
+
integration for hosts that supply an activity stream directly.
|
|
84
|
+
|
|
85
|
+
## Examples and checks
|
|
86
|
+
|
|
87
|
+
The activity tests cover environment gating, activity changes, cleanup on scope
|
|
88
|
+
close, and report failure handling. Run them with:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pnpm --filter @fungi.computer/corral test
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Limits and errors
|
|
95
|
+
|
|
96
|
+
Corral is trusted local extension code, not a sandbox. It does not start or stop
|
|
97
|
+
the Herdr service and does not decide what counts as successful work. Report and
|
|
98
|
+
release requests are optional IPC: each is limited to two seconds. Failures are
|
|
99
|
+
passed to `onError`. A reporting failure does not veto client activity or
|
|
100
|
+
terminal restoration. The caller owns any user-facing recovery.
|
|
101
|
+
|
|
102
|
+
The wire contract follows Herdr's
|
|
103
|
+
[agent integration guide](https://herdr.dev/docs/add-herdr-support/) and its
|
|
104
|
+
[Pi extension](https://github.com/herdrdev/herdr/blob/v0.9.3/src/integration/assets/pi/herdr-agent-state.ts).
|
|
105
|
+
|
|
106
|
+
## Contribute and license
|
|
107
|
+
|
|
108
|
+
Corral is MIT licensed. See [`LICENSE`](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ExtensionId, type ClientActivityV0 } from "@fungi.computer/shiitake/extensions";
|
|
2
|
+
import * as Stream from "effect/Stream";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import type * as Scope from "effect/Scope";
|
|
5
|
+
/** Pane environment supplied by the local Herdr host. */
|
|
6
|
+
export type CorralEnvironment = Readonly<{
|
|
7
|
+
/** Herdr sets this to "1" for managed processes. */
|
|
8
|
+
HERDR_ENV?: string | undefined;
|
|
9
|
+
/** Opaque pane identity inherited by the client. */
|
|
10
|
+
HERDR_PANE_ID?: string | undefined;
|
|
11
|
+
/** Local API socket (or named pipe on Windows). */
|
|
12
|
+
HERDR_SOCKET_PATH?: string | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
/** Activity supplied by the client; blocked means a human response is pending. */
|
|
15
|
+
export type CorralActivity = ClientActivityV0;
|
|
16
|
+
/** Attach to borrowed activity; never opens a Session watch or starts Herdr. */
|
|
17
|
+
export declare function attachCorral(activity: Stream.Stream<CorralActivity, Error>, options: Readonly<{
|
|
18
|
+
environment: CorralEnvironment;
|
|
19
|
+
onError: (cause: Error) => void;
|
|
20
|
+
}>): Effect.Effect<void, Error, Scope.Scope>;
|
|
21
|
+
/** Ordinary Fungi client extension; disabled outside a Herdr pane. */
|
|
22
|
+
declare const extension: Readonly<{
|
|
23
|
+
protocol: "shiitake.client/v1";
|
|
24
|
+
id: ExtensionId;
|
|
25
|
+
promptPresentation?: "footer";
|
|
26
|
+
attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => Effect.Effect<void, Error, Scope.Scope>;
|
|
27
|
+
contribute?: (slot: import("@fungi.computer/shiitake/extensions").ExtensionSlotV0 | undefined) => import("@fungi.computer/shiitake/client-entry").ClientContributionV1;
|
|
28
|
+
}>;
|
|
29
|
+
export default extension;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineClientEntry } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { createConnection } from "node:net";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { ExtensionId, } from "@fungi.computer/shiitake/extensions";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
const source = "fungi:agent:v1";
|
|
11
|
+
let reportSequence = Date.now() * 1000;
|
|
12
|
+
const decodeResponse = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Union([
|
|
13
|
+
Schema.Struct({ id: Schema.String, result: Schema.Unknown }),
|
|
14
|
+
Schema.Struct({
|
|
15
|
+
id: Schema.String,
|
|
16
|
+
error: Schema.Struct({ code: Schema.String, message: Schema.String }),
|
|
17
|
+
}),
|
|
18
|
+
])));
|
|
19
|
+
function nextSequence() {
|
|
20
|
+
reportSequence = Math.max(reportSequence + 1, Date.now() * 1000);
|
|
21
|
+
return reportSequence;
|
|
22
|
+
}
|
|
23
|
+
function request(endpoint, pane, activity) {
|
|
24
|
+
const state = activity?.state;
|
|
25
|
+
const seq = nextSequence();
|
|
26
|
+
const id = `${source}:${String(seq)}`;
|
|
27
|
+
const params = {
|
|
28
|
+
pane_id: pane,
|
|
29
|
+
source,
|
|
30
|
+
agent: "fungi",
|
|
31
|
+
seq,
|
|
32
|
+
};
|
|
33
|
+
if (state !== undefined)
|
|
34
|
+
params.state = state;
|
|
35
|
+
if (state === "blocked")
|
|
36
|
+
params.message =
|
|
37
|
+
activity?.header === undefined
|
|
38
|
+
? "Waiting for your response"
|
|
39
|
+
: `Waiting for your response: ${activity.header}`;
|
|
40
|
+
const body = {
|
|
41
|
+
id,
|
|
42
|
+
method: state === undefined ? "pane.release_agent" : "pane.report_agent",
|
|
43
|
+
params,
|
|
44
|
+
};
|
|
45
|
+
return Effect.tryPromise({
|
|
46
|
+
try: async (signal) => {
|
|
47
|
+
const socket = createConnection({ path: endpoint, signal });
|
|
48
|
+
const lines = createInterface({ input: socket, crlfDelay: Infinity });
|
|
49
|
+
try {
|
|
50
|
+
const replies = lines[Symbol.asyncIterator]();
|
|
51
|
+
const [, reply] = await Promise.all([
|
|
52
|
+
once(socket, "connect", { signal }).then(() => {
|
|
53
|
+
socket.write(`${JSON.stringify(body)}\n`);
|
|
54
|
+
}),
|
|
55
|
+
replies.next(),
|
|
56
|
+
]);
|
|
57
|
+
if (reply.done)
|
|
58
|
+
throw new Error("Herdr closed without a response");
|
|
59
|
+
const response = decodeResponse(reply.value);
|
|
60
|
+
if (response.id !== id)
|
|
61
|
+
throw new Error("Herdr response ID mismatch");
|
|
62
|
+
if ("error" in response)
|
|
63
|
+
throw new Error(`Herdr report refused: ${response.error.code}`);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
lines.close();
|
|
67
|
+
socket.destroy();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
catch: (cause) => new Error("Herdr report failed", { cause }),
|
|
71
|
+
}).pipe(Effect.timeout("2 seconds"));
|
|
72
|
+
}
|
|
73
|
+
/** Attach to borrowed activity; never opens a Session watch or starts Herdr. */
|
|
74
|
+
export function attachCorral(activity, options) {
|
|
75
|
+
const { HERDR_ENV, HERDR_SOCKET_PATH, HERDR_PANE_ID } = options.environment;
|
|
76
|
+
if (HERDR_ENV !== "1" || !HERDR_SOCKET_PATH || !HERDR_PANE_ID)
|
|
77
|
+
return Effect.void;
|
|
78
|
+
const endpoint = process.platform === "win32"
|
|
79
|
+
? `\\\\.\\pipe\\${HERDR_SOCKET_PATH}`
|
|
80
|
+
: HERDR_SOCKET_PATH;
|
|
81
|
+
const send = (activity) => request(endpoint, HERDR_PANE_ID, activity).pipe(Effect.catch((cause) => Effect.sync(() => {
|
|
82
|
+
options.onError(cause);
|
|
83
|
+
})));
|
|
84
|
+
return Effect.gen(function* () {
|
|
85
|
+
yield* Effect.acquireRelease(Effect.void, () => send(undefined));
|
|
86
|
+
yield* activity.pipe(Stream.changesWith((a, b) => a.state === b.state && a.header === b.header), Stream.runForEach(send), Effect.catch((cause) => Effect.sync(() => {
|
|
87
|
+
options.onError(cause);
|
|
88
|
+
})), Effect.forkScoped);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Ordinary Fungi client extension; disabled outside a Herdr pane. */
|
|
92
|
+
const extension = defineClientEntry({
|
|
93
|
+
id: ExtensionId.make("corral"),
|
|
94
|
+
commands: {},
|
|
95
|
+
projection: null,
|
|
96
|
+
}, {
|
|
97
|
+
attach: ({ activity, onError }) => attachCorral(Stream.fromAsyncIterable(activity, (cause) => new Error("Corral activity delivery failed", { cause })), {
|
|
98
|
+
environment: {
|
|
99
|
+
HERDR_ENV: process.env.HERDR_ENV,
|
|
100
|
+
HERDR_SOCKET_PATH: process.env.HERDR_SOCKET_PATH,
|
|
101
|
+
HERDR_PANE_ID: process.env.HERDR_PANE_ID,
|
|
102
|
+
},
|
|
103
|
+
onError,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
export default extension;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
|
|
2
|
+
|
|
3
|
+
**@fungi.computer/corral**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @fungi.computer/corral
|
|
8
|
+
|
|
9
|
+
## Type Aliases
|
|
10
|
+
|
|
11
|
+
### CorralEnvironment
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
type CorralEnvironment = Readonly<{
|
|
15
|
+
HERDR_ENV?: string;
|
|
16
|
+
HERDR_PANE_ID?: string;
|
|
17
|
+
HERDR_SOCKET_PATH?: string;
|
|
18
|
+
}>;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Pane environment supplied by the local Herdr host.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### CorralActivity
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type CorralActivity = ClientActivityV0;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Activity supplied by the client; blocked means a human response is pending.
|
|
32
|
+
|
|
33
|
+
## Variables
|
|
34
|
+
|
|
35
|
+
### default
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const default: Readonly<{
|
|
39
|
+
protocol: "shiitake.client/v1";
|
|
40
|
+
id: string & Brand<"ExtensionId">;
|
|
41
|
+
promptPresentation?: "footer";
|
|
42
|
+
attach?: (context) => Effect<void, Error, Scope>;
|
|
43
|
+
contribute?: (slot) => object;
|
|
44
|
+
}>;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Ordinary Fungi client extension; disabled outside a Herdr pane.
|
|
48
|
+
|
|
49
|
+
## Functions
|
|
50
|
+
|
|
51
|
+
### attachCorral()
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
function attachCorral(activity, options): Effect<void, Error, Scope>;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Attach to borrowed activity; never opens a Session watch or starts Herdr.
|
|
58
|
+
|
|
59
|
+
#### Parameters
|
|
60
|
+
|
|
61
|
+
| Parameter | Type |
|
|
62
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `activity` | `Stream`\<`Readonly`\<\{ `state`: `ClientActivityStateV0`; `header?`: `string`; \}\>, `Error`\> |
|
|
64
|
+
| `options` | `Readonly`\<\{ `environment`: [`CorralEnvironment`](#corralenvironment); `onError`: (`cause`) => `void`; \}\> |
|
|
65
|
+
|
|
66
|
+
#### Returns
|
|
67
|
+
|
|
68
|
+
`Effect`\<`void`, `Error`, `Scope`\>
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/corral",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Optional Herdr activity reporting for Shiitake clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"docs/api",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"effect": "4.0.2",
|
|
21
|
+
"@fungi.computer/shiitake": "0.3.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "24.13.3",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"vitest": "4.1.11"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "Levi <levi@technomancy.dev>",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"botanical": {
|
|
33
|
+
"role": "package",
|
|
34
|
+
"operations": {
|
|
35
|
+
"lint": "required",
|
|
36
|
+
"typecheck": "required",
|
|
37
|
+
"build": "required",
|
|
38
|
+
"test": "required",
|
|
39
|
+
"docs": "required",
|
|
40
|
+
"pack": "required",
|
|
41
|
+
"smoke": "required",
|
|
42
|
+
"stranger": "required"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/fungi-computer/corral.git"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"lint": "eslint src test --max-warnings=0",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"docs": "tsx ../../tooling/repository/src/docs/cli.ts check --root ../..",
|
|
55
|
+
"docs:write": "tsx ../../tooling/repository/src/docs/cli.ts write --root ../..",
|
|
56
|
+
"pack": "mkdir -p ../../.botanical/corral-pack && pnpm pack --pack-destination ../../.botanical/corral-pack",
|
|
57
|
+
"smoke": "node --input-type=module -e \"import('./dist/index.js').then(({ attachCorral }) => { if (typeof attachCorral !== 'function') process.exit(1); })\"",
|
|
58
|
+
"stranger": "tsx ../../tooling/repository/src/artifacts/cli.ts --repo-root ../.. .",
|
|
59
|
+
"format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
|
|
60
|
+
"lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs"
|
|
61
|
+
}
|
|
6
62
|
}
|