@quantiya/codevibe-gemini-plugin 1.0.19 → 1.0.21
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/bin/codevibe-gemini +199 -7
- package/package.json +1 -1
package/bin/codevibe-gemini
CHANGED
|
@@ -43,6 +43,89 @@ done
|
|
|
43
43
|
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
44
44
|
PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
|
|
45
45
|
|
|
46
|
+
# ─── PATH augmentation ───────────────────────────────────────────────
|
|
47
|
+
# When the install one-liner runs in a fresh terminal, Homebrew's
|
|
48
|
+
# installer writes the shellenv eval into ~/.zprofile (and similar)
|
|
49
|
+
# but the user's current shell hasn't sourced it yet. Subsequent
|
|
50
|
+
# codevibe-* runs in that same terminal then fail tmux discovery
|
|
51
|
+
# because /opt/homebrew/bin isn't on PATH. Prepend common locations
|
|
52
|
+
# so the wrapper recovers without forcing the user to open a new
|
|
53
|
+
# terminal. Prepend (not append) is deliberate: the Homebrew binary
|
|
54
|
+
# install.sh just laid down should win over any older system binary
|
|
55
|
+
# (e.g. a stale /usr/bin/node on Linux) at the same name. To preserve
|
|
56
|
+
# the relative ordering of augmented dirs, build a single prefix
|
|
57
|
+
# string and prepend it once — iterating prepend-per-dir would
|
|
58
|
+
# reverse intended order. ${PATH:+:$PATH} keeps an empty starting
|
|
59
|
+
# PATH from producing a trailing colon (which makes cwd searchable).
|
|
60
|
+
_CV_NEW_PATHS=""
|
|
61
|
+
for _CV_DIR in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin /opt/local/bin /usr/bin /bin; do
|
|
62
|
+
case ":$PATH:" in
|
|
63
|
+
*":$_CV_DIR:"*) ;;
|
|
64
|
+
*) [ -d "$_CV_DIR" ] && _CV_NEW_PATHS="$_CV_NEW_PATHS:$_CV_DIR" ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
[ -n "$_CV_NEW_PATHS" ] && export PATH="${_CV_NEW_PATHS#:}${PATH:+:$PATH}"
|
|
68
|
+
unset _CV_DIR _CV_NEW_PATHS
|
|
69
|
+
|
|
70
|
+
# ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
|
|
71
|
+
# Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
|
|
72
|
+
# whether SessionStart hook fired, exit code. Background curl, fail
|
|
73
|
+
# silently, no PII (hashed hostname + per-run random id only). Honors
|
|
74
|
+
# CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
|
|
75
|
+
_CV_MID="G-GS74YEQTB8"
|
|
76
|
+
_CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
|
|
77
|
+
_CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
|
|
78
|
+
_CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
|
|
79
|
+
[ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
|
|
80
|
+
_CV_AGENT="gemini"
|
|
81
|
+
_CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
|
|
82
|
+
_CV_STARTED_AT="$(date +%s)"
|
|
83
|
+
_CV_EXITED="" # set by terminal events; suppresses trap double-fire
|
|
84
|
+
_CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
|
|
85
|
+
_CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
|
|
86
|
+
_CV_MCP_LOG_BASELINE=0
|
|
87
|
+
if [ -f "$_CV_MCP_LOG" ]; then
|
|
88
|
+
_CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
|
|
89
|
+
[ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
|
|
90
|
+
fi
|
|
91
|
+
_CV_TMUX_STARTED="false"
|
|
92
|
+
_CV_AGENT_INVOKED="false"
|
|
93
|
+
_CV_AGENT_STARTED_AT=0
|
|
94
|
+
_CV_GEMINI_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-exit-$$"
|
|
95
|
+
|
|
96
|
+
# Strip an arbitrary string down to a JSON-safe identifier alphabet.
|
|
97
|
+
# Removes anything that could break the hand-built JSON payload below
|
|
98
|
+
# (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
|
|
99
|
+
# Truncates to 40 chars to bound the impact of pathological CLI version
|
|
100
|
+
# output. Caller is responsible for emptiness check after sanitize.
|
|
101
|
+
cv_sanitize() {
|
|
102
|
+
printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Sanitize trusted-but-still-string values that go into the payload
|
|
106
|
+
# (plugin version, source label) so future schema additions can't
|
|
107
|
+
# accidentally reintroduce a JSON-injection path.
|
|
108
|
+
_CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
|
|
109
|
+
[ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
|
|
110
|
+
_CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
|
|
111
|
+
[ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
|
|
112
|
+
|
|
113
|
+
cv_telem() {
|
|
114
|
+
local event="$1"; shift
|
|
115
|
+
local params="$*"
|
|
116
|
+
curl -s -X POST \
|
|
117
|
+
"https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
|
|
118
|
+
-H "Content-Type: application/json" \
|
|
119
|
+
-d "{\"client_id\":\"${_CV_CID}\",\"events\":[{\"name\":\"${event}\",\"params\":{\"agent\":\"${_CV_AGENT}\",\"plugin_version\":\"${_CV_PLUGIN_VERSION}\",\"source\":\"${_CV_SOURCE}\",\"run_id\":\"${_CV_RUN_ID}\"${params:+,$params}}}]}" \
|
|
120
|
+
</dev/null >/dev/null 2>&1 &
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cv_failed() {
|
|
124
|
+
[ -n "$_CV_EXITED" ] && return 0
|
|
125
|
+
_CV_EXITED="failed"
|
|
126
|
+
cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
|
|
127
|
+
}
|
|
128
|
+
|
|
46
129
|
# Handle auth commands (login, logout, status, reset-device)
|
|
47
130
|
# Delegate to codevibe-core CLI (shared auth across all plugins)
|
|
48
131
|
case "$1" in
|
|
@@ -53,14 +136,42 @@ case "$1" in
|
|
|
53
136
|
CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
|
|
54
137
|
fi
|
|
55
138
|
if [ -f "$CORE_CLI" ]; then
|
|
139
|
+
cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
|
|
56
140
|
exec node "$CORE_CLI" "$1"
|
|
57
141
|
else
|
|
58
142
|
echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
|
|
143
|
+
cv_failed "core_not_found"
|
|
144
|
+
sleep 1
|
|
59
145
|
exit 1
|
|
60
146
|
fi
|
|
61
147
|
;;
|
|
62
148
|
esac
|
|
63
149
|
|
|
150
|
+
# Capture environment facts for the session-flow wrapper_started event.
|
|
151
|
+
# Each probe is non-fatal — if a CLI is missing we record "missing" rather
|
|
152
|
+
# than aborting; pre-flight checks below still gate execution. Every
|
|
153
|
+
# string that lands in the JSON payload goes through cv_sanitize so an
|
|
154
|
+
# agent CLI emitting ANSI escapes or quotes in `--version` can't break
|
|
155
|
+
# the hand-built payload.
|
|
156
|
+
_CV_GEMINI_VER="missing"
|
|
157
|
+
command -v gemini >/dev/null 2>&1 && _CV_GEMINI_VER="$(gemini --version 2>/dev/null | cv_sanitize)"
|
|
158
|
+
[ -z "$_CV_GEMINI_VER" ] && _CV_GEMINI_VER="unknown"
|
|
159
|
+
_CV_NODE_VER="missing"
|
|
160
|
+
command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
|
|
161
|
+
[ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
|
|
162
|
+
_CV_TMUX_VER="missing"
|
|
163
|
+
command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
|
|
164
|
+
[ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
|
|
165
|
+
_CV_OS_VER="$(uname -s | cv_sanitize)"
|
|
166
|
+
[ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
|
|
167
|
+
_CV_ARCH_VER="$(uname -m | cv_sanitize)"
|
|
168
|
+
[ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
|
|
169
|
+
_CV_GEMINI_AUTH="false"; [ -f "$HOME/.gemini/oauth_creds.json" ] && _CV_GEMINI_AUTH="true"
|
|
170
|
+
_CV_GEMINI_SETTINGS="false"; [ -f "$HOME/.gemini/settings.json" ] && _CV_GEMINI_SETTINGS="true"
|
|
171
|
+
_CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
|
|
172
|
+
_CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
|
|
173
|
+
cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"gemini_version\":\"$_CV_GEMINI_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"gemini_auth_present\":$_CV_GEMINI_AUTH,\"gemini_settings_present\":$_CV_GEMINI_SETTINGS,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"
|
|
174
|
+
|
|
64
175
|
# Export hooks directory for hook scripts to use
|
|
65
176
|
export CODEVIBE_HOOKS_DIR="$PLUGIN_DIR/hooks"
|
|
66
177
|
|
|
@@ -130,7 +241,52 @@ log() {
|
|
|
130
241
|
|
|
131
242
|
# Cleanup function to kill MCP server when wrapper exits
|
|
132
243
|
cleanup() {
|
|
244
|
+
local wrapper_exit_code=$?
|
|
133
245
|
log "Cleanup triggered"
|
|
246
|
+
|
|
247
|
+
# Fire wrapper_exited telemetry BEFORE killing the server so the MCP
|
|
248
|
+
# log is intact when we grep for SessionStart. cv_failed sets
|
|
249
|
+
# _CV_EXITED on pre-flight failures so this block won't double-fire.
|
|
250
|
+
if [ -z "$_CV_EXITED" ]; then
|
|
251
|
+
_CV_EXITED="exited"
|
|
252
|
+
local gemini_exit="unknown"
|
|
253
|
+
if [ -f "$_CV_GEMINI_EXIT_FILE" ]; then
|
|
254
|
+
gemini_exit="$(cat "$_CV_GEMINI_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
|
|
255
|
+
[ -z "$gemini_exit" ] && gemini_exit="unknown"
|
|
256
|
+
fi
|
|
257
|
+
local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
|
|
258
|
+
local gemini_lifetime=0
|
|
259
|
+
if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
|
|
260
|
+
gemini_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
|
|
261
|
+
fi
|
|
262
|
+
local hook_fired="false"
|
|
263
|
+
if [ -f "$_CV_MCP_LOG" ]; then
|
|
264
|
+
if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
|
|
265
|
+
| grep -q "SessionStart" 2>/dev/null; then
|
|
266
|
+
hook_fired="true"
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
# Outcome priority: SIGINT/SIGTERM beats everything (user intent).
|
|
270
|
+
# Then "we never got far enough to invoke gemini" — distinct from
|
|
271
|
+
# "we invoked gemini via passthrough but never started a tmux of
|
|
272
|
+
# our own" (the latter is a normal direct-run, not an abort).
|
|
273
|
+
local outcome
|
|
274
|
+
if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
|
|
275
|
+
outcome="interrupted"
|
|
276
|
+
elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
|
|
277
|
+
outcome="pre_invoke_abort"
|
|
278
|
+
elif [ "$gemini_exit" != "unknown" ] && [ "$gemini_exit" != "0" ]; then
|
|
279
|
+
outcome="error_exit"
|
|
280
|
+
elif [ "$gemini_lifetime" -lt 5 ] 2>/dev/null; then
|
|
281
|
+
outcome="early_exit"
|
|
282
|
+
elif [ "$gemini_lifetime" -lt 60 ] 2>/dev/null; then
|
|
283
|
+
outcome="clean_short"
|
|
284
|
+
else
|
|
285
|
+
outcome="clean_long"
|
|
286
|
+
fi
|
|
287
|
+
cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"gemini_exit_code\":\"$gemini_exit\",\"gemini_lifetime_seconds\":$gemini_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
|
|
288
|
+
fi
|
|
289
|
+
|
|
134
290
|
if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
|
|
135
291
|
log "Stopping MCP server (PID: $MCP_PID)"
|
|
136
292
|
kill "$MCP_PID" 2>/dev/null || true
|
|
@@ -138,6 +294,7 @@ cleanup() {
|
|
|
138
294
|
fi
|
|
139
295
|
# Remove PID file
|
|
140
296
|
rm -f "${CODEVIBE_TMPDIR}/codevibe-gemini-mcp-$$.pid"
|
|
297
|
+
rm -f "$_CV_GEMINI_EXIT_FILE"
|
|
141
298
|
}
|
|
142
299
|
|
|
143
300
|
# Set up trap for cleanup
|
|
@@ -147,6 +304,8 @@ trap cleanup EXIT INT TERM
|
|
|
147
304
|
if ! command -v tmux &> /dev/null; then
|
|
148
305
|
echo "Error: tmux is required but not installed."
|
|
149
306
|
echo "Install with: brew install tmux"
|
|
307
|
+
cv_failed "tmux_missing"
|
|
308
|
+
sleep 1
|
|
150
309
|
exit 1
|
|
151
310
|
fi
|
|
152
311
|
|
|
@@ -154,18 +313,24 @@ fi
|
|
|
154
313
|
if ! command -v gemini &> /dev/null; then
|
|
155
314
|
echo "Error: gemini CLI is not installed."
|
|
156
315
|
echo "Install from: https://github.com/google-gemini/gemini-cli"
|
|
316
|
+
cv_failed "gemini_missing"
|
|
317
|
+
sleep 1
|
|
157
318
|
exit 1
|
|
158
319
|
fi
|
|
159
320
|
|
|
160
321
|
# Check if node is installed
|
|
161
322
|
if ! command -v node &> /dev/null; then
|
|
162
323
|
echo "Error: Node.js is required but not installed."
|
|
324
|
+
cv_failed "node_missing"
|
|
325
|
+
sleep 1
|
|
163
326
|
exit 1
|
|
164
327
|
fi
|
|
165
328
|
|
|
166
329
|
# Check if MCP server is built
|
|
167
330
|
if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
|
|
168
331
|
echo "Error: MCP server not built. Run 'npm run build' in the plugin directory first."
|
|
332
|
+
cv_failed "server_not_built"
|
|
333
|
+
sleep 1
|
|
169
334
|
exit 1
|
|
170
335
|
fi
|
|
171
336
|
|
|
@@ -177,17 +342,36 @@ log "Starting codevibe-gemini with session: $SESSION_NAME"
|
|
|
177
342
|
log "Working directory: $WORKING_DIR"
|
|
178
343
|
log "Arguments: $*"
|
|
179
344
|
|
|
180
|
-
# Check if we're already inside tmux
|
|
345
|
+
# Check if we're already inside tmux.
|
|
346
|
+
# We deliberately do NOT `exec` here — running gemini as a child process
|
|
347
|
+
# lets the EXIT trap fire after it returns so wrapper_exited still gets
|
|
348
|
+
# emitted on these direct-run paths. Behaviorally identical for the user
|
|
349
|
+
# (gemini remains the foreground process for the duration).
|
|
181
350
|
if [ -n "$TMUX" ]; then
|
|
182
351
|
log "Already inside tmux, running gemini directly"
|
|
183
|
-
|
|
184
|
-
|
|
352
|
+
_CV_AGENT_INVOKED="true"
|
|
353
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
354
|
+
# `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit
|
|
355
|
+
# from gemini would abort the wrapper before we capture the exit
|
|
356
|
+
# code, leaving wrapper_exited with gemini_exit_code="unknown". The
|
|
357
|
+
# `||` form catches non-zero without triggering set -e, while exit
|
|
358
|
+
# 0 leaves _CV_RC at its 0 default. printf's `|| true` keeps a
|
|
359
|
+
# disk-full failure from clobbering diagnostics.
|
|
360
|
+
_CV_RC=0
|
|
361
|
+
gemini "$@" || _CV_RC=$?
|
|
362
|
+
printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
|
|
363
|
+
exit "$_CV_RC"
|
|
185
364
|
fi
|
|
186
365
|
|
|
187
|
-
# Check if running in a terminal
|
|
366
|
+
# Check if running in a terminal — same direct-run treatment as above.
|
|
188
367
|
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
189
368
|
log "Not running in a terminal, running gemini directly"
|
|
190
|
-
|
|
369
|
+
_CV_AGENT_INVOKED="true"
|
|
370
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
371
|
+
_CV_RC=0
|
|
372
|
+
gemini "$@" || _CV_RC=$?
|
|
373
|
+
printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
|
|
374
|
+
exit "$_CV_RC"
|
|
191
375
|
fi
|
|
192
376
|
|
|
193
377
|
# Start MCP server in background BEFORE launching Gemini
|
|
@@ -214,6 +398,8 @@ if ! kill -0 "$MCP_PID" 2>/dev/null; then
|
|
|
214
398
|
tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
|
|
215
399
|
echo ""
|
|
216
400
|
echo "Server failed to start. Check $MCP_LOG_FILE for details."
|
|
401
|
+
cv_failed "server_died_on_startup"
|
|
402
|
+
sleep 1
|
|
217
403
|
exit 1
|
|
218
404
|
fi
|
|
219
405
|
|
|
@@ -232,10 +418,16 @@ done
|
|
|
232
418
|
# We use a wrapper that:
|
|
233
419
|
# 1. Exports the session name so prompts can find it
|
|
234
420
|
# 2. Runs gemini
|
|
235
|
-
# 3.
|
|
421
|
+
# 3. Captures gemini's exit code to $_CV_GEMINI_EXIT_FILE for the
|
|
422
|
+
# wrapper's cleanup trap (tmux's own attach exit code is independent
|
|
423
|
+
# of the inner process exit, so the file is the only reliable signal)
|
|
424
|
+
# 4. Exits the tmux session when gemini exits
|
|
236
425
|
|
|
237
426
|
tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
|
|
238
|
-
"export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; exit"
|
|
427
|
+
"export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; printf '%s' \"\$?\" > '$_CV_GEMINI_EXIT_FILE'; exit"
|
|
428
|
+
_CV_TMUX_STARTED="true"
|
|
429
|
+
_CV_AGENT_INVOKED="true"
|
|
430
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
239
431
|
|
|
240
432
|
# Enable mouse support for scrolling
|
|
241
433
|
tmux set-option -t "$SESSION_NAME" -g mouse on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-gemini-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "Control Gemini CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|