agentera 0.0.0 → 0.0.2
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 +25 -2
- package/bin/agentera.mjs +3 -0
- package/lib/exec.mjs +79 -16
- package/lib/resolve.mjs +138 -11
- package/package.json +18 -14
package/README.md
CHANGED
|
@@ -12,6 +12,29 @@ npx agentera prime
|
|
|
12
12
|
npm install -g agentera
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## v3 hint
|
|
16
|
+
|
|
17
|
+
While the Python CLI remains on `@latest`, every 0.x shim invocation emits a
|
|
18
|
+
three-line deprecation hint to **stderr** telling the user the v3 TypeScript
|
|
19
|
+
CLI is ready on the `@next` tag:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
agentera 2.x (Python) is in maintenance; the v3 TypeScript CLI is ready on the @next tag:
|
|
23
|
+
npx -y agentera@next prime
|
|
24
|
+
Set AGENTERA_NO_V3_HINT=1 to suppress this message.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The hint never appears on `--version`/`-V` and never touches stdout, so
|
|
28
|
+
`--json` and pipe consumers stay clean. `--help`/`-h` also lists the `@next`
|
|
29
|
+
pointer, and the install-help failure path (no backend available) leads with
|
|
30
|
+
the `@next` line.
|
|
31
|
+
|
|
32
|
+
Suppress the hint for non-interactive CI or scripted invocations:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
AGENTERA_NO_V3_HINT=1 npx agentera prime
|
|
36
|
+
```
|
|
37
|
+
|
|
15
38
|
## Delegation order
|
|
16
39
|
|
|
17
40
|
When you run `agentera`, the shim forwards to the existing Python CLI using the first match:
|
|
@@ -41,8 +64,8 @@ npx agentera@0.0.0 --version
|
|
|
41
64
|
## Development
|
|
42
65
|
|
|
43
66
|
```bash
|
|
44
|
-
node packages/cli/bin/agentera.mjs --version
|
|
45
|
-
node packages/cli/bin/agentera.mjs --help
|
|
67
|
+
node packages/cli/shim/bin/agentera.mjs --version
|
|
68
|
+
node packages/cli/shim/bin/agentera.mjs --help
|
|
46
69
|
```
|
|
47
70
|
|
|
48
71
|
Suite version pin lives in `package.json` under `agentera.suiteVersion` / `agentera.gitRef`; npm `version` stays on the `0.0.x` line until 3.0.
|
package/bin/agentera.mjs
CHANGED
|
@@ -35,6 +35,9 @@ if (userArgs[0] === "--help" || userArgs[0] === "-h") {
|
|
|
35
35
|
console.log(" 2. scripts/agentera in a parent repo (uv run)");
|
|
36
36
|
console.log(" 3. uvx --from git+https://github.com/jgabor/agentera@<tag> agentera");
|
|
37
37
|
console.log("");
|
|
38
|
+
console.log("v3 (TypeScript) is available now on the @next tag:");
|
|
39
|
+
console.log(" npx -y agentera@next prime");
|
|
40
|
+
console.log("");
|
|
38
41
|
console.log("https://github.com/jgabor/agentera");
|
|
39
42
|
process.exit(0);
|
|
40
43
|
}
|
package/lib/exec.mjs
CHANGED
|
@@ -1,36 +1,68 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
import { resolveBackend } from "./resolve.mjs";
|
|
5
4
|
|
|
5
|
+
const V3_NEXT_COMMAND = "npx -y agentera@next prime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Print the v3 deprecation hint to stderr unless the user opted out via
|
|
9
|
+
* AGENTERA_NO_V3_HINT=1. Called once per dispatch (after the --version
|
|
10
|
+
* early-return) so every v2 invocation surfaces the @next pointer while the
|
|
11
|
+
* Python CLI remains on @latest.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ logStderr?: (message: string) => void }} [options]
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
export function printV3Hint(options = {}) {
|
|
17
|
+
if (process.env.AGENTERA_NO_V3_HINT === "1") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const log = options.logStderr ?? ((msg) => console.error(msg));
|
|
21
|
+
log("agentera 2.x (Python) is in maintenance; the v3 TypeScript CLI is ready on the @next tag:");
|
|
22
|
+
log(` ${V3_NEXT_COMMAND}`);
|
|
23
|
+
log("Set AGENTERA_NO_V3_HINT=1 to suppress this message.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} RunBackendResult
|
|
28
|
+
* @property {number} exitCode
|
|
29
|
+
* @property {boolean} fallthrough true when the backend is unusable and dispatch should try the next strategy
|
|
30
|
+
*/
|
|
31
|
+
|
|
6
32
|
/**
|
|
7
33
|
* @param {import('./resolve.mjs').ResolveResult} backend
|
|
8
34
|
* @param {string[]} args
|
|
9
|
-
* @param {{ gitRef?: string; gitRepo?: string }} [meta]
|
|
10
|
-
* @returns {
|
|
35
|
+
* @param {{ gitRef?: string; gitRepo?: string; cwd?: string }} [meta]
|
|
36
|
+
* @returns {RunBackendResult}
|
|
11
37
|
*/
|
|
12
38
|
export function runBackend(backend, args, meta = {}) {
|
|
13
39
|
if (backend.kind === "app-home" && backend.scriptPath) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
cwd: appRoot,
|
|
40
|
+
const exitCode = spawnChecked("uv", ["run", backend.scriptPath, ...args], {
|
|
41
|
+
cwd: meta.cwd,
|
|
17
42
|
});
|
|
43
|
+
return { exitCode, fallthrough: exitCode !== 0 };
|
|
18
44
|
}
|
|
19
45
|
|
|
20
46
|
if (backend.kind === "repo" && backend.repoRoot) {
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
return {
|
|
48
|
+
exitCode: spawnChecked("uv", ["run", "scripts/agentera", ...args], {
|
|
49
|
+
cwd: backend.repoRoot,
|
|
50
|
+
}),
|
|
51
|
+
fallthrough: false,
|
|
52
|
+
};
|
|
24
53
|
}
|
|
25
54
|
|
|
26
55
|
if (backend.kind === "uvx") {
|
|
27
|
-
const gitRef = backend.gitRef ?? meta.gitRef ?? "v2.7.
|
|
56
|
+
const gitRef = backend.gitRef ?? meta.gitRef ?? "v2.7.7";
|
|
28
57
|
const gitRepo = backend.gitRepo ?? meta.gitRepo ?? "https://github.com/jgabor/agentera";
|
|
29
58
|
const from = `git+${gitRepo}@${gitRef}`;
|
|
30
|
-
return
|
|
59
|
+
return {
|
|
60
|
+
exitCode: spawnChecked("uvx", ["--from", from, "agentera", ...args], {}),
|
|
61
|
+
fallthrough: false,
|
|
62
|
+
};
|
|
31
63
|
}
|
|
32
64
|
|
|
33
|
-
return 1;
|
|
65
|
+
return { exitCode: 1, fallthrough: false };
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
/**
|
|
@@ -63,6 +95,7 @@ function spawnChecked(command, args, options) {
|
|
|
63
95
|
* @param {string} [options.gitRef]
|
|
64
96
|
* @param {string} [options.gitRepo]
|
|
65
97
|
* @param {(message: string) => void} [options.printInstallHelp]
|
|
98
|
+
* @param {(message: string) => void} [options.logStderr]
|
|
66
99
|
* @returns {number}
|
|
67
100
|
*/
|
|
68
101
|
export function dispatch(argv, options = {}) {
|
|
@@ -71,13 +104,37 @@ export function dispatch(argv, options = {}) {
|
|
|
71
104
|
return 0;
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
|
|
107
|
+
printV3Hint({ logStderr: options.logStderr });
|
|
108
|
+
|
|
109
|
+
let backend = resolveBackend({
|
|
75
110
|
cwd: options.cwd,
|
|
76
111
|
env: options.env,
|
|
77
112
|
gitRef: options.gitRef,
|
|
78
113
|
gitRepo: options.gitRepo,
|
|
114
|
+
logStderr: options.logStderr,
|
|
79
115
|
});
|
|
80
116
|
|
|
117
|
+
if (backend.kind === "app-home") {
|
|
118
|
+
const result = runBackend(backend, userArgs, {
|
|
119
|
+
gitRef: options.gitRef,
|
|
120
|
+
gitRepo: options.gitRepo,
|
|
121
|
+
cwd: options.cwd,
|
|
122
|
+
});
|
|
123
|
+
if (!result.fallthrough) {
|
|
124
|
+
return result.exitCode;
|
|
125
|
+
}
|
|
126
|
+
console.error(
|
|
127
|
+
`agentera: app-home backend crashed (exit ${result.exitCode}); falling through to next resolution strategy`,
|
|
128
|
+
);
|
|
129
|
+
backend = resolveBackend({
|
|
130
|
+
cwd: options.cwd,
|
|
131
|
+
env: options.env,
|
|
132
|
+
gitRef: options.gitRef,
|
|
133
|
+
gitRepo: options.gitRepo,
|
|
134
|
+
excludeAppHome: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
81
138
|
if (backend.kind === "none") {
|
|
82
139
|
const print = options.printInstallHelp ?? printInstallHelp;
|
|
83
140
|
print(backend.reason ?? "no backend available");
|
|
@@ -87,7 +144,8 @@ export function dispatch(argv, options = {}) {
|
|
|
87
144
|
return runBackend(backend, userArgs, {
|
|
88
145
|
gitRef: options.gitRef,
|
|
89
146
|
gitRepo: options.gitRepo,
|
|
90
|
-
|
|
147
|
+
cwd: options.cwd,
|
|
148
|
+
}).exitCode;
|
|
91
149
|
}
|
|
92
150
|
|
|
93
151
|
/**
|
|
@@ -97,6 +155,11 @@ export function printInstallHelp(reason) {
|
|
|
97
155
|
const lines = [
|
|
98
156
|
"agentera: npm CLI shim (0.x) — native TypeScript CLI ships in Agentera 3.0.",
|
|
99
157
|
reason ? `agentera: ${reason}` : "",
|
|
158
|
+
];
|
|
159
|
+
if (process.env.AGENTERA_NO_V3_HINT !== "1") {
|
|
160
|
+
lines.push("", "The v3 TypeScript CLI is ready now on the @next tag:", ` ${V3_NEXT_COMMAND}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push(
|
|
100
163
|
"",
|
|
101
164
|
"Install Agentera for your runtime:",
|
|
102
165
|
" npx skills add jgabor/agentera -g -a claude-code --skill agentera -y",
|
|
@@ -109,8 +172,8 @@ export function printInstallHelp(reason) {
|
|
|
109
172
|
" uv run scripts/agentera prime",
|
|
110
173
|
"",
|
|
111
174
|
"https://github.com/jgabor/agentera#install",
|
|
112
|
-
|
|
113
|
-
for (const line of lines) {
|
|
175
|
+
);
|
|
176
|
+
for (const line of lines.filter(Boolean)) {
|
|
114
177
|
console.error(line);
|
|
115
178
|
}
|
|
116
179
|
}
|
package/lib/resolve.mjs
CHANGED
|
@@ -5,6 +5,9 @@ const DEFAULT_GIT_REPO = "https://github.com/jgabor/agentera";
|
|
|
5
5
|
const APP_SCRIPT_REL = path.join("app", "scripts", "agentera");
|
|
6
6
|
const REPO_SCRIPT_REL = path.join("scripts", "agentera");
|
|
7
7
|
|
|
8
|
+
const HEAD_READ_BYTES = 2048;
|
|
9
|
+
const APP_HOME_MISMATCH_PREFIX = "agentera: app-home backend unavailable: shebang/content mismatch";
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* @typedef {'app-home' | 'repo' | 'uvx' | 'none'} BackendKind
|
|
10
13
|
*/
|
|
@@ -19,6 +22,16 @@ const REPO_SCRIPT_REL = path.join("scripts", "agentera");
|
|
|
19
22
|
* @property {string} [reason]
|
|
20
23
|
*/
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} ResolveOptions
|
|
27
|
+
* @property {string} [cwd]
|
|
28
|
+
* @property {NodeJS.ProcessEnv} [env]
|
|
29
|
+
* @property {string} [gitRef]
|
|
30
|
+
* @property {string} [gitRepo]
|
|
31
|
+
* @property {boolean} [excludeAppHome]
|
|
32
|
+
* @property {(message: string) => void} [logStderr]
|
|
33
|
+
*/
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* @param {string | undefined} command
|
|
24
37
|
* @returns {boolean}
|
|
@@ -56,6 +69,110 @@ export function isRunnableFile(filePath) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} filePath
|
|
74
|
+
* @returns {string | null}
|
|
75
|
+
*/
|
|
76
|
+
function readHead(filePath) {
|
|
77
|
+
let fd;
|
|
78
|
+
try {
|
|
79
|
+
fd = fs.openSync(filePath, "r");
|
|
80
|
+
const buf = Buffer.alloc(HEAD_READ_BYTES);
|
|
81
|
+
const bytes = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
|
|
82
|
+
return buf.slice(0, bytes).toString("utf8");
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
} finally {
|
|
86
|
+
if (fd !== undefined) {
|
|
87
|
+
try {
|
|
88
|
+
fs.closeSync(fd);
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} shebang
|
|
98
|
+
* @returns {'js' | 'python' | 'unknown'}
|
|
99
|
+
*/
|
|
100
|
+
function shebangRuntime(shebang) {
|
|
101
|
+
if (/\bnode\b/.test(shebang)) {
|
|
102
|
+
return "js";
|
|
103
|
+
}
|
|
104
|
+
if (/\bpython\d*\b/.test(shebang) || /\buv\b[^\n]*\brun\b/.test(shebang)) {
|
|
105
|
+
return "python";
|
|
106
|
+
}
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} body
|
|
112
|
+
* @returns {'js' | 'python' | 'unknown'}
|
|
113
|
+
*/
|
|
114
|
+
function contentLanguage(body) {
|
|
115
|
+
if (body.includes("# /// script")) {
|
|
116
|
+
return "python";
|
|
117
|
+
}
|
|
118
|
+
if (/\brequire\s*\(/.test(body) || /\bmodule\.exports\b/.test(body)) {
|
|
119
|
+
return "js";
|
|
120
|
+
}
|
|
121
|
+
if (/^\s*import\s+[^'"\n]+\s+from\s+['"]/m.test(body)) {
|
|
122
|
+
return "js";
|
|
123
|
+
}
|
|
124
|
+
if (/^\s*import\s+['"]/m.test(body)) {
|
|
125
|
+
return "js";
|
|
126
|
+
}
|
|
127
|
+
const lines = body.split("\n").slice(0, 30);
|
|
128
|
+
for (const raw of lines) {
|
|
129
|
+
const t = raw.trimStart();
|
|
130
|
+
if (
|
|
131
|
+
/^import\s+\w+\s*$/.test(t) ||
|
|
132
|
+
t.startsWith("from ") ||
|
|
133
|
+
t.startsWith("def ") ||
|
|
134
|
+
t.startsWith("class ")
|
|
135
|
+
) {
|
|
136
|
+
return "python";
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
t.startsWith("const ") ||
|
|
140
|
+
t.startsWith("let ") ||
|
|
141
|
+
t.startsWith("var ") ||
|
|
142
|
+
t.startsWith("export ")
|
|
143
|
+
) {
|
|
144
|
+
return "js";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return "unknown";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {string} filePath
|
|
152
|
+
* @returns {string | null}
|
|
153
|
+
*/
|
|
154
|
+
export function detectShebangContentMismatch(filePath) {
|
|
155
|
+
const head = readHead(filePath);
|
|
156
|
+
if (head === null) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const nl = head.indexOf("\n");
|
|
160
|
+
const shebang = (nl >= 0 ? head.slice(0, nl) : head).trim();
|
|
161
|
+
if (!shebang.startsWith("#!")) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const body = nl >= 0 ? head.slice(nl + 1) : "";
|
|
165
|
+
const expected = shebangRuntime(shebang);
|
|
166
|
+
const actual = contentLanguage(body);
|
|
167
|
+
if (expected === "unknown" || actual === "unknown") {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (expected !== actual) {
|
|
171
|
+
return `shebang=${expected} content=${actual}`;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
59
176
|
/**
|
|
60
177
|
* @param {string} startDir
|
|
61
178
|
* @returns {string | null}
|
|
@@ -77,33 +194,43 @@ export function findRepoRoot(startDir) {
|
|
|
77
194
|
|
|
78
195
|
/**
|
|
79
196
|
* @param {string | undefined} agenteraHome
|
|
197
|
+
* @param {{ logStderr?: (message: string) => void }} [options]
|
|
80
198
|
* @returns {string | null}
|
|
81
199
|
*/
|
|
82
|
-
export function findAppHomeScript(agenteraHome) {
|
|
200
|
+
export function findAppHomeScript(agenteraHome, options = {}) {
|
|
83
201
|
if (!agenteraHome) {
|
|
84
202
|
return null;
|
|
85
203
|
}
|
|
86
204
|
const scriptPath = path.join(path.resolve(agenteraHome), APP_SCRIPT_REL);
|
|
87
|
-
|
|
205
|
+
if (!isRunnableFile(scriptPath)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const mismatch = detectShebangContentMismatch(scriptPath);
|
|
209
|
+
if (mismatch) {
|
|
210
|
+
const log = options.logStderr ?? ((msg) => console.error(msg));
|
|
211
|
+
log(`${APP_HOME_MISMATCH_PREFIX} (${mismatch}) at ${scriptPath}`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return scriptPath;
|
|
88
215
|
}
|
|
89
216
|
|
|
90
217
|
/**
|
|
91
|
-
* @param {
|
|
92
|
-
* @param {string} [options.cwd]
|
|
93
|
-
* @param {NodeJS.ProcessEnv} [options.env]
|
|
94
|
-
* @param {string} [options.gitRef]
|
|
95
|
-
* @param {string} [options.gitRepo]
|
|
218
|
+
* @param {ResolveOptions} [options]
|
|
96
219
|
* @returns {ResolveResult}
|
|
97
220
|
*/
|
|
98
221
|
export function resolveBackend({
|
|
99
222
|
cwd = process.cwd(),
|
|
100
223
|
env = process.env,
|
|
101
|
-
gitRef = "v2.7.
|
|
224
|
+
gitRef = "v2.7.7",
|
|
102
225
|
gitRepo = DEFAULT_GIT_REPO,
|
|
226
|
+
excludeAppHome = false,
|
|
227
|
+
logStderr,
|
|
103
228
|
} = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
229
|
+
if (!excludeAppHome) {
|
|
230
|
+
const scriptPath = findAppHomeScript(env.AGENTERA_HOME, { logStderr });
|
|
231
|
+
if (scriptPath) {
|
|
232
|
+
return { kind: "app-home", scriptPath };
|
|
233
|
+
}
|
|
107
234
|
}
|
|
108
235
|
|
|
109
236
|
const repoRoot = findRepoRoot(cwd);
|
package/package.json
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentera",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "npm shim for Agentera — delegates to the Python CLI until the 3.0 TypeScript release",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "Apache-2.0",
|
|
7
|
-
"author": "Jonathan Gabor",
|
|
8
|
-
"homepage": "https://github.com/jgabor/agentera",
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/jgabor/agentera.git",
|
|
12
|
-
"directory": "packages/cli"
|
|
13
|
-
},
|
|
14
5
|
"keywords": [
|
|
15
6
|
"agentera",
|
|
16
7
|
"agents",
|
|
17
8
|
"cli",
|
|
18
9
|
"orchestration"
|
|
19
10
|
],
|
|
20
|
-
"
|
|
21
|
-
|
|
11
|
+
"homepage": "https://github.com/jgabor/agentera",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"author": "Jonathan Gabor",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/jgabor/agentera.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
22
18
|
},
|
|
23
19
|
"bin": {
|
|
24
20
|
"agentera": "./bin/agentera.mjs"
|
|
@@ -29,8 +25,16 @@
|
|
|
29
25
|
"README.md",
|
|
30
26
|
"LICENSE"
|
|
31
27
|
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "pnpm -C .. test test/shim/ && node --test test/*.test.mjs",
|
|
31
|
+
"publish:stable": "node ./scripts/publish-stable.mjs"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
32
36
|
"agentera": {
|
|
33
|
-
"suiteVersion": "2.7.
|
|
34
|
-
"gitRef": "
|
|
37
|
+
"suiteVersion": "2.7.7",
|
|
38
|
+
"gitRef": "ce4a7054a8438b8b0aac013bac4b86ba1a9de3dd"
|
|
35
39
|
}
|
|
36
40
|
}
|