@quantiya/codevibe-antigravity-plugin 2.0.83 → 2.0.85
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 +5 -4
- package/dist/agy-hook.js +191 -18
- package/dist/installer-cli.js +14 -3
- package/dist/server.js +2145 -1318
- package/libexec/agy-hook.sh +23 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,20 +53,21 @@ install the unified `codevibe --agent` interface yet.
|
|
|
53
53
|
|
|
54
54
|
Antigravity CLI writes session transcripts to `~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript.jsonl`. CodeVibe follows the active conversation's transcript file, parses every line of the JSONL stream, and pushes events through E2E-encrypted AWS AppSync to your phone.
|
|
55
55
|
|
|
56
|
-
Approval prompts and multi-choice questions aren't in the transcript at the moment they appear — agy renders them directly into the terminal. CodeVibe observes the live tmux pane to
|
|
56
|
+
Approval prompts and multi-choice questions aren't in the transcript at the moment they appear — agy renders them directly into the terminal. CodeVibe captures interactive questions immediately at the lifecycle layer via a scoped `PreToolUse` hook and observes the live tmux pane to parse actionable option labels, surfacing them to mobile. Your mobile reply is delivered back via `tmux send-keys`.
|
|
57
57
|
|
|
58
58
|
A single launch session covers the entire wrapper lifetime, so `/resume` commands continue under the same backend session and your phone sees one continuous conversation.
|
|
59
59
|
|
|
60
60
|
### Several agy sessions at once
|
|
61
61
|
|
|
62
|
-
All agy sessions on a computer write to the same folders. CodeVibe tells them apart with
|
|
62
|
+
All agy sessions on a computer write to the same folders. CodeVibe tells them apart with three agy hooks:
|
|
63
63
|
- **`PreInvocation`** reports which conversation a session is running.
|
|
64
|
-
- **`
|
|
64
|
+
- **`PreToolUse`** (scoped strictly to `^(?:ask_question|question|ask)$`) captures interactive questions with an immediate `{"decision":"allow"}` non-blocking exit.
|
|
65
|
+
- **`PostToolUse`** reports which tool just ran. When that tool was waiting on an approval or question you answered at the desktop, the prompt on your phone closes.
|
|
65
66
|
|
|
66
67
|
On each launch, `codevibe --agent agy` registers one named entry, `codevibe-antigravity`, in `~/.gemini/config/hooks.json`, and keeps the hook scripts in `~/.codevibe/codevibe-agy/hooks/`:
|
|
67
68
|
- Other entries in that file are left as they are.
|
|
68
69
|
- A file that is not valid JSON, or is a symlink, is never changed. CodeVibe then falls back to agy's log files, which cannot tell concurrent sessions apart.
|
|
69
|
-
-
|
|
70
|
+
- For other agy sessions on the machine, `PreToolUse` immediately permits execution with `{"decision":"allow"}`, and other hook events are skipped.
|
|
70
71
|
- To switch it off, add `"enabled": false` to the `codevibe-antigravity` entry. CodeVibe leaves a switched-off entry alone. Deleting the entry only lasts until the next `codevibe --agent agy` launch, which writes it again. After uninstalling CodeVibe you can delete the entry; see INSTALLATION.md.
|
|
71
72
|
|
|
72
73
|
## What gets synced
|
package/dist/agy-hook.js
CHANGED
|
@@ -32,13 +32,17 @@ var agy_hook_exports = {};
|
|
|
32
32
|
__export(agy_hook_exports, {
|
|
33
33
|
FORWARDED_TOOL_ARG_KEYS: () => FORWARDED_TOOL_ARG_KEYS,
|
|
34
34
|
HARD_DEADLINE_MS: () => HARD_DEADLINE_MS,
|
|
35
|
+
MAX_HOOK_BODY_BYTES: () => MAX_HOOK_BODY_BYTES,
|
|
36
|
+
MAX_STRUCTURED_ARG_CHARS: () => MAX_STRUCTURED_ARG_CHARS,
|
|
35
37
|
REQUEST_TIMEOUT_MS: () => REQUEST_TIMEOUT_MS,
|
|
36
38
|
buildBody: () => buildBody,
|
|
39
|
+
decodeBoolean: () => decodeBoolean,
|
|
37
40
|
isPidAlive: () => isPidAlive,
|
|
38
41
|
postHook: () => postHook,
|
|
39
42
|
readConn: () => readConn,
|
|
40
43
|
readStdin: () => readStdin,
|
|
41
|
-
runHook: () => runHook
|
|
44
|
+
runHook: () => runHook,
|
|
45
|
+
serializeStructuredArg: () => serializeStructuredArg
|
|
42
46
|
});
|
|
43
47
|
module.exports = __toCommonJS(agy_hook_exports);
|
|
44
48
|
var fs = __toESM(require("fs"));
|
|
@@ -49,15 +53,15 @@ var path2 = __toESM(require("path"));
|
|
|
49
53
|
var os = __toESM(require("os"));
|
|
50
54
|
var path = __toESM(require("path"));
|
|
51
55
|
var HOOK_ROUTE_PREFIX = "/agy-hook/v1/";
|
|
52
|
-
var HOOK_EVENTS = ["PreInvocation", "PostToolUse"];
|
|
56
|
+
var HOOK_EVENTS = ["PreInvocation", "PostToolUse", "PreToolUse", "Stop"];
|
|
53
57
|
var HOOK_OWNER_ENV = "CODEVIBE_AGY_HOOK_OWNER";
|
|
54
58
|
function isHookEvent(value) {
|
|
55
59
|
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
56
60
|
}
|
|
57
|
-
function codevibeAgyDir(home = os.homedir()) {
|
|
61
|
+
function codevibeAgyDir(home = process.env.HOME || os.homedir()) {
|
|
58
62
|
return path.join(home, ".codevibe", "codevibe-agy");
|
|
59
63
|
}
|
|
60
|
-
function runtimeBaseDir(home = os.homedir()) {
|
|
64
|
+
function runtimeBaseDir(home = process.env.HOME || os.homedir()) {
|
|
61
65
|
return path.join(codevibeAgyDir(home), "runtime");
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -67,6 +71,8 @@ var REQUEST_TIMEOUT_MS = 1500;
|
|
|
67
71
|
var MAX_STDIN_BYTES = 1024 * 1024;
|
|
68
72
|
var MAX_CONN_FILE_BYTES = 4096;
|
|
69
73
|
var MAX_TOOL_ARG_CHARS = 4096;
|
|
74
|
+
var MAX_STRUCTURED_ARG_CHARS = 16384;
|
|
75
|
+
var MAX_HOOK_BODY_BYTES = 48 * 1024;
|
|
70
76
|
var FORWARDED_TOOL_ARG_KEYS = [
|
|
71
77
|
"CommandLine",
|
|
72
78
|
"Command",
|
|
@@ -77,8 +83,116 @@ var FORWARDED_TOOL_ARG_KEYS = [
|
|
|
77
83
|
"file_path",
|
|
78
84
|
"filePath",
|
|
79
85
|
"path",
|
|
80
|
-
"DirectoryPath"
|
|
86
|
+
"DirectoryPath",
|
|
87
|
+
"questions",
|
|
88
|
+
"question",
|
|
89
|
+
"prompt",
|
|
90
|
+
"options",
|
|
91
|
+
"is_multi_select"
|
|
81
92
|
];
|
|
93
|
+
function decodeBoolean(val) {
|
|
94
|
+
if (typeof val === "boolean") return val;
|
|
95
|
+
if (typeof val === "string") {
|
|
96
|
+
const s = val.trim().toLowerCase();
|
|
97
|
+
return s === "true" || s === "1" || s === "yes";
|
|
98
|
+
}
|
|
99
|
+
if (typeof val === "number") return val !== 0;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyHint) {
|
|
103
|
+
try {
|
|
104
|
+
let parsed = value;
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
const trimmed = value.trim();
|
|
107
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(trimmed);
|
|
110
|
+
} catch {
|
|
111
|
+
return value.slice(0, maxChars);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
return value.slice(0, maxChars);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(parsed)) {
|
|
118
|
+
const isOptionArray = keyHint === "questions" || keyHint === "question" ? false : keyHint === "options" || keyHint === "option" || !keyHint && parsed.length > 0 && parsed.every(
|
|
119
|
+
(item) => typeof item === "string" || typeof item === "number" || item && typeof item === "object" && !("question" in item)
|
|
120
|
+
);
|
|
121
|
+
if (isOptionArray) {
|
|
122
|
+
const boundedOptions = parsed.slice(0, 30).map((opt, idx) => {
|
|
123
|
+
if (typeof opt === "string") return { number: String(idx + 1), text: opt.slice(0, 256) };
|
|
124
|
+
if (typeof opt === "number") return { number: String(idx + 1), text: String(opt) };
|
|
125
|
+
if (opt && typeof opt === "object") {
|
|
126
|
+
const optObj = opt;
|
|
127
|
+
const num = typeof optObj.number !== "undefined" ? String(optObj.number).slice(0, 16) : String(idx + 1);
|
|
128
|
+
const txt = typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256);
|
|
129
|
+
return { number: num, text: txt };
|
|
130
|
+
}
|
|
131
|
+
return { number: String(idx + 1), text: String(opt ?? "").slice(0, 256) };
|
|
132
|
+
});
|
|
133
|
+
const str2 = JSON.stringify(boundedOptions);
|
|
134
|
+
if (str2.length <= maxChars) return str2;
|
|
135
|
+
for (let i = boundedOptions.length - 1; i >= 1; i--) {
|
|
136
|
+
const sub = JSON.stringify(boundedOptions.slice(0, i));
|
|
137
|
+
if (sub.length <= maxChars) return sub;
|
|
138
|
+
}
|
|
139
|
+
const firstOpt = boundedOptions[0];
|
|
140
|
+
if (firstOpt) {
|
|
141
|
+
const safeText = firstOpt.text.slice(0, Math.max(0, maxChars - 30));
|
|
142
|
+
return JSON.stringify([{ number: firstOpt.number, text: safeText }]);
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify(boundedOptions.slice(0, 1));
|
|
145
|
+
}
|
|
146
|
+
const bounded = parsed.slice(0, 20).map((item) => {
|
|
147
|
+
if (typeof item === "string") return { question: item.slice(0, 1024), options: [] };
|
|
148
|
+
if (item && typeof item === "object") {
|
|
149
|
+
const res = {};
|
|
150
|
+
const obj = item;
|
|
151
|
+
if (typeof obj.question === "string") res.question = obj.question.slice(0, 1024);
|
|
152
|
+
if (typeof obj.is_multi_select !== "undefined") res.is_multi_select = decodeBoolean(obj.is_multi_select);
|
|
153
|
+
if (typeof obj.isMultiSelect !== "undefined") res.isMultiSelect = decodeBoolean(obj.isMultiSelect);
|
|
154
|
+
if (Array.isArray(obj.options)) {
|
|
155
|
+
res.options = obj.options.slice(0, 25).map((opt, idx) => {
|
|
156
|
+
if (typeof opt === "string") return opt.slice(0, 256);
|
|
157
|
+
if (typeof opt === "number") return String(opt);
|
|
158
|
+
if (opt && typeof opt === "object") {
|
|
159
|
+
const optObj = opt;
|
|
160
|
+
return {
|
|
161
|
+
...typeof optObj.number !== "undefined" ? { number: String(optObj.number).slice(0, 16) } : {},
|
|
162
|
+
text: typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return String(opt ?? "").slice(0, 256);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return res;
|
|
169
|
+
}
|
|
170
|
+
return { question: String(item).slice(0, 1024), options: [] };
|
|
171
|
+
});
|
|
172
|
+
let str = JSON.stringify(bounded);
|
|
173
|
+
if (str.length <= maxChars) return str;
|
|
174
|
+
for (let i = bounded.length - 1; i >= 1; i--) {
|
|
175
|
+
const sub = JSON.stringify(bounded.slice(0, i));
|
|
176
|
+
if (sub.length <= maxChars) return sub;
|
|
177
|
+
}
|
|
178
|
+
const firstQ = bounded[0];
|
|
179
|
+
if (firstQ && typeof firstQ === "object") {
|
|
180
|
+
const qText = typeof firstQ.question === "string" ? firstQ.question : "";
|
|
181
|
+
const safeQText = qText.slice(0, Math.max(0, maxChars - 30));
|
|
182
|
+
return JSON.stringify([{ question: safeQText, options: [] }]);
|
|
183
|
+
}
|
|
184
|
+
return JSON.stringify(bounded.slice(0, 1));
|
|
185
|
+
}
|
|
186
|
+
if (parsed && typeof parsed === "object") {
|
|
187
|
+
const raw = JSON.stringify(parsed);
|
|
188
|
+
if (raw.length <= maxChars) return raw;
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
82
196
|
function buildBody(raw) {
|
|
83
197
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
84
198
|
const payload = raw;
|
|
@@ -98,10 +212,46 @@ function buildBody(raw) {
|
|
|
98
212
|
for (const key of FORWARDED_TOOL_ARG_KEYS) {
|
|
99
213
|
const value = source[key];
|
|
100
214
|
if (typeof value === "string") {
|
|
101
|
-
|
|
215
|
+
if (key === "questions" || key === "options") {
|
|
216
|
+
const serialized = serializeStructuredArg(value, MAX_STRUCTURED_ARG_CHARS, key);
|
|
217
|
+
if (serialized !== null) kept[key] = serialized;
|
|
218
|
+
} else {
|
|
219
|
+
kept[key] = value.slice(0, MAX_TOOL_ARG_CHARS);
|
|
220
|
+
}
|
|
221
|
+
} else if (value !== null && typeof value === "object") {
|
|
222
|
+
const serialized = serializeStructuredArg(value, MAX_STRUCTURED_ARG_CHARS, key);
|
|
223
|
+
if (serialized !== null) {
|
|
224
|
+
kept[key] = serialized;
|
|
225
|
+
}
|
|
226
|
+
} else if (typeof value === "boolean" || typeof value === "number") {
|
|
227
|
+
kept[key] = String(value);
|
|
102
228
|
}
|
|
103
229
|
}
|
|
104
230
|
body.toolArgs = kept;
|
|
231
|
+
let totalBytes = Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
232
|
+
if (totalBytes > MAX_HOOK_BODY_BYTES && kept.questions) {
|
|
233
|
+
try {
|
|
234
|
+
const qs = JSON.parse(kept.questions);
|
|
235
|
+
if (Array.isArray(qs) && qs.length > 1) {
|
|
236
|
+
kept.questions = JSON.stringify(qs.slice(0, 1));
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
totalBytes = Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
242
|
+
if (totalBytes > MAX_HOOK_BODY_BYTES) {
|
|
243
|
+
let firstQText = kept.question;
|
|
244
|
+
if (!firstQText && kept.questions) {
|
|
245
|
+
try {
|
|
246
|
+
const parsedQs = JSON.parse(kept.questions);
|
|
247
|
+
if (Array.isArray(parsedQs) && parsedQs[0]?.question) {
|
|
248
|
+
firstQText = String(parsedQs[0].question);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
body.toolArgs = firstQText ? { question: firstQText.slice(0, 512) } : {};
|
|
254
|
+
}
|
|
105
255
|
}
|
|
106
256
|
}
|
|
107
257
|
if ("error" in payload) {
|
|
@@ -169,15 +319,21 @@ function postHook(conn, event, body) {
|
|
|
169
319
|
},
|
|
170
320
|
(response) => {
|
|
171
321
|
response.resume();
|
|
172
|
-
response.on("end", () =>
|
|
173
|
-
|
|
322
|
+
response.on("end", () => {
|
|
323
|
+
finish((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300);
|
|
324
|
+
});
|
|
325
|
+
response.on("error", () => {
|
|
326
|
+
finish(false);
|
|
327
|
+
});
|
|
174
328
|
}
|
|
175
329
|
);
|
|
176
330
|
request2.on("timeout", () => {
|
|
177
331
|
request2.destroy();
|
|
178
332
|
finish(false);
|
|
179
333
|
});
|
|
180
|
-
request2.on("error", () =>
|
|
334
|
+
request2.on("error", () => {
|
|
335
|
+
finish(false);
|
|
336
|
+
});
|
|
181
337
|
request2.end(payload);
|
|
182
338
|
} catch {
|
|
183
339
|
finish(false);
|
|
@@ -210,9 +366,13 @@ function readStdin(stream) {
|
|
|
210
366
|
async function runHook(options) {
|
|
211
367
|
const owner = options.env[HOOK_OWNER_ENV] ?? "";
|
|
212
368
|
const raw = await readStdin(options.stdin);
|
|
213
|
-
if (!/^[0-9]{1,10}$/.test(owner) || !isHookEvent(options.event) || raw === null)
|
|
214
|
-
|
|
215
|
-
|
|
369
|
+
if (!/^[0-9]{1,10}$/.test(owner) || !isHookEvent(options.event) || raw === null) {
|
|
370
|
+
return "skipped";
|
|
371
|
+
}
|
|
372
|
+
const conn = readConn(options.runtimeBase ?? runtimeBaseDir(options.env.HOME), owner);
|
|
373
|
+
if (!conn || !(options.isPidAlive ?? isPidAlive)(conn.pid)) {
|
|
374
|
+
return "skipped";
|
|
375
|
+
}
|
|
216
376
|
let parsed;
|
|
217
377
|
try {
|
|
218
378
|
parsed = JSON.parse(raw);
|
|
@@ -220,31 +380,44 @@ async function runHook(options) {
|
|
|
220
380
|
return "skipped";
|
|
221
381
|
}
|
|
222
382
|
const body = buildBody(parsed);
|
|
223
|
-
if (!body)
|
|
383
|
+
if (!body) {
|
|
384
|
+
return "skipped";
|
|
385
|
+
}
|
|
224
386
|
const ok = await (options.post ?? postHook)(conn, options.event, body);
|
|
225
387
|
return ok ? "sent" : "failed";
|
|
226
388
|
}
|
|
227
|
-
function finishProcess() {
|
|
389
|
+
function finishProcess(event) {
|
|
228
390
|
try {
|
|
229
|
-
|
|
391
|
+
let payload = "{}";
|
|
392
|
+
if (event === "PreToolUse") {
|
|
393
|
+
payload = '{"decision":"allow"}';
|
|
394
|
+
} else if (event === "Stop") {
|
|
395
|
+
payload = '{"decision":"stop"}';
|
|
396
|
+
}
|
|
397
|
+
fs.writeSync(1, payload);
|
|
230
398
|
} catch {
|
|
231
399
|
}
|
|
232
400
|
process.exit(0);
|
|
233
401
|
}
|
|
234
402
|
if (require.main === module) {
|
|
235
|
-
const
|
|
403
|
+
const event = process.argv[2];
|
|
404
|
+
const exitHandler = () => finishProcess(event);
|
|
236
405
|
setTimeout(exitHandler, HARD_DEADLINE_MS);
|
|
237
|
-
runHook({ event
|
|
406
|
+
runHook({ event, env: process.env, stdin: process.stdin }).then(exitHandler, exitHandler);
|
|
238
407
|
}
|
|
239
408
|
// Annotate the CommonJS export names for ESM import in node:
|
|
240
409
|
0 && (module.exports = {
|
|
241
410
|
FORWARDED_TOOL_ARG_KEYS,
|
|
242
411
|
HARD_DEADLINE_MS,
|
|
412
|
+
MAX_HOOK_BODY_BYTES,
|
|
413
|
+
MAX_STRUCTURED_ARG_CHARS,
|
|
243
414
|
REQUEST_TIMEOUT_MS,
|
|
244
415
|
buildBody,
|
|
416
|
+
decodeBoolean,
|
|
245
417
|
isPidAlive,
|
|
246
418
|
postHook,
|
|
247
419
|
readConn,
|
|
248
420
|
readStdin,
|
|
249
|
-
runHook
|
|
421
|
+
runHook,
|
|
422
|
+
serializeStructuredArg
|
|
250
423
|
});
|
package/dist/installer-cli.js
CHANGED
|
@@ -48,13 +48,13 @@ var path = __toESM(require("path"));
|
|
|
48
48
|
var HOOK_ENTRY_NAME = "codevibe-antigravity";
|
|
49
49
|
var HOOK_SHELL_BASENAME = "agy-hook.sh";
|
|
50
50
|
var HOOK_SCRIPT_BASENAME = "agy-hook.js";
|
|
51
|
-
function codevibeAgyDir(home = os.homedir()) {
|
|
51
|
+
function codevibeAgyDir(home = process.env.HOME || os.homedir()) {
|
|
52
52
|
return path.join(home, ".codevibe", "codevibe-agy");
|
|
53
53
|
}
|
|
54
|
-
function hooksDir(home = os.homedir()) {
|
|
54
|
+
function hooksDir(home = process.env.HOME || os.homedir()) {
|
|
55
55
|
return path.join(codevibeAgyDir(home), "hooks");
|
|
56
56
|
}
|
|
57
|
-
function globalHooksConfigPath(home = os.homedir()) {
|
|
57
|
+
function globalHooksConfigPath(home = process.env.HOME || os.homedir()) {
|
|
58
58
|
return path.join(home, ".gemini", "config", "hooks.json");
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -328,6 +328,14 @@ function buildHookEntry(hooksFolder) {
|
|
|
328
328
|
PreInvocation: [
|
|
329
329
|
{ type: "command", command: `${shell} PreInvocation`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
330
330
|
],
|
|
331
|
+
PreToolUse: [
|
|
332
|
+
{
|
|
333
|
+
matcher: "^(?:ask_question|question|ask)$",
|
|
334
|
+
hooks: [
|
|
335
|
+
{ type: "command", command: `${shell} PreToolUse`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
],
|
|
331
339
|
PostToolUse: [
|
|
332
340
|
{
|
|
333
341
|
matcher: "*",
|
|
@@ -335,6 +343,9 @@ function buildHookEntry(hooksFolder) {
|
|
|
335
343
|
{ type: "command", command: `${shell} PostToolUse`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
336
344
|
]
|
|
337
345
|
}
|
|
346
|
+
],
|
|
347
|
+
Stop: [
|
|
348
|
+
{ type: "command", command: `${shell} Stop`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
338
349
|
]
|
|
339
350
|
};
|
|
340
351
|
}
|