claude-multiacc 1.0.13 → 1.0.14
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 +249 -14
- package/bin/claude +402 -38
- package/bin/claude-accounts +141 -45
- package/bin/codex +254 -26
- package/bin/codex-accounts +157 -52
- package/install.sh +175 -69
- package/lib/common.sh +262 -2
- package/lib/credential.py +336 -0
- package/lib/report.py +355 -0
- package/package.json +1 -1
- package/tests/run-tests.sh +1053 -1
package/bin/claude-accounts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# claude-accounts — manage the claude-multiacc account pool.
|
|
3
3
|
# Subcommands: list status add import remove mint sync verify limits post-sync health
|
|
4
|
-
# expired relogin
|
|
4
|
+
# expired relogin export-credential import-credential
|
|
5
5
|
set -u
|
|
6
6
|
# lib/audit.py is imported by several subcommands; keep the install tree free of
|
|
7
7
|
# __pycache__ (it may be root-owned, read-only, or an npm global prefix).
|
|
@@ -22,8 +22,8 @@ usage() {
|
|
|
22
22
|
claude-accounts — multi-account pool manager for claude-multiacc
|
|
23
23
|
|
|
24
24
|
USAGE
|
|
25
|
-
claude-accounts list
|
|
26
|
-
claude-accounts status
|
|
25
|
+
claude-accounts list [--json] brief account list (--json: machine-readable)
|
|
26
|
+
claude-accounts status [--json] full health: auth, per-bucket limits, markers
|
|
27
27
|
claude-accounts add [email] [--token] [--force]
|
|
28
28
|
login-FIRST: runs the full Claude Code login (`claude auth login` — the normal
|
|
29
29
|
browser sign-in, no long-lived-token step-up), then registers only after the
|
|
@@ -51,17 +51,34 @@ USAGE
|
|
|
51
51
|
claude-accounts adopt <acct-NN> make acct-NN THIS machine's existing
|
|
52
52
|
default ~/.claude login (dir symlink —
|
|
53
53
|
single credential file, no grant fork)
|
|
54
|
+
claude-accounts export-credential <acct-NN> [--out PATH] [--identity-only]
|
|
55
|
+
print a self-contained JSON blob (credential + identity metadata) for a
|
|
56
|
+
PORTABLE account, i.e. one with a setup-token. REFUSES a machine-local OAuth
|
|
57
|
+
credential (exit 3) — those cannot be copied; sign in on the machine that
|
|
58
|
+
needs them, or run 'mint <acct-NN>' once to make the account portable.
|
|
59
|
+
--identity-only exports registry metadata with NO credential material.
|
|
60
|
+
Exit: 0 ok, 3 machine-local, 4 no credential, 5 unusable material.
|
|
61
|
+
claude-accounts import-credential [acct-NN] [--in PATH|-] [--home mac|server]
|
|
62
|
+
[--force] [--no-sync]
|
|
63
|
+
install a blob from export-credential (stdin by default): creates the account
|
|
64
|
+
dir + manifest entry as needed, or refreshes the credential of the account
|
|
65
|
+
that already owns that email. Non-interactive — this is how a daemon
|
|
66
|
+
distributes accounts to a machine.
|
|
54
67
|
claude-accounts remove <acct-NN> [--yes] delete account (propagates to server)
|
|
55
68
|
claude-accounts dedupe [--yes] remove any account registered twice
|
|
56
69
|
(same email), keeping one per email
|
|
57
70
|
claude-accounts mint <acct-NN> mint server token via `claude setup-token`
|
|
58
71
|
--paste paste an already-minted token instead of running setup-token
|
|
59
|
-
claude-accounts sync
|
|
60
|
-
manifest 'peers' (extra machines). Mac
|
|
61
|
-
A pool with a 'sync-role' file saying
|
|
72
|
+
claude-accounts sync [--no-server] push manifest+tokens to the sync target AND
|
|
73
|
+
any manifest 'peers' (extra machines). Mac
|
|
74
|
+
only. A pool with a 'sync-role' file saying
|
|
62
75
|
'replica' never pushes (it receives).
|
|
76
|
+
--no-server (or a pool whose target is
|
|
77
|
+
'none') keeps sync LOCAL: validate + seed +
|
|
78
|
+
fix perms, push nowhere — for pools a panel
|
|
79
|
+
or runner daemon distributes.
|
|
63
80
|
claude-accounts verify [--quick] auth matrix; full mode runs `-p "reply OK"` per account
|
|
64
|
-
claude-accounts limits [--quiet] [--force]
|
|
81
|
+
claude-accounts limits [--quiet] [--force] [--json]
|
|
65
82
|
refresh usage buckets, apply >=90% markers. Auto-refreshes long-expired
|
|
66
83
|
OAuth access tokens via the refresh-token grant (rotated credential is
|
|
67
84
|
persisted), so idle accounts keep fresh telemetry and stay selectable.
|
|
@@ -73,7 +90,12 @@ USAGE
|
|
|
73
90
|
claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
74
91
|
|
|
75
92
|
ENV
|
|
76
|
-
|
|
93
|
+
CLAUDE_ACCOUNTS_ROOT pool root, overriding ~/.claude-accounts — one isolated pool
|
|
94
|
+
per app-robot instance on a shared machine (legacy spelling
|
|
95
|
+
CLAUDE_ACCOUNTS_DIR still works)
|
|
96
|
+
CLAUDE_MULTIACC_SYNC_TARGET sync target (user@host), overriding the manifest;
|
|
97
|
+
'none' = local-only, nothing is pushed anywhere
|
|
98
|
+
CLAUDE_MULTIACC_SYNC_ROOT / _SYNC_REPO remote pool root / addon repo for it
|
|
77
99
|
CLAUDE_ACCOUNT pin the shim to one account
|
|
78
100
|
CLAUDE_SHIM_RETRY=0 disable -p auto-retry
|
|
79
101
|
CLAUDE_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
@@ -95,17 +117,19 @@ next_id() {
|
|
|
95
117
|
done
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
manifest_add_account() { # id email home
|
|
99
|
-
"$PYBIN" - "$MANIFEST" "$1" "$2" "$3" <<'PYEOF'
|
|
120
|
+
manifest_add_account() { # id email home [added_at]
|
|
121
|
+
"$PYBIN" - "$MANIFEST" "$1" "$2" "$3" "${4:-}" <<'PYEOF'
|
|
100
122
|
import json, sys, time
|
|
101
|
-
path, aid, email, home = sys.argv[1:
|
|
123
|
+
path, aid, email, home, added_at = sys.argv[1:6]
|
|
102
124
|
doc = json.load(open(path))
|
|
103
125
|
accounts = [a for a in doc.get('accounts', []) if a['id'] != aid]
|
|
104
126
|
accounts.append({
|
|
105
127
|
'id': aid,
|
|
106
128
|
'email': email,
|
|
107
129
|
'home': home,
|
|
108
|
-
|
|
130
|
+
# An imported account keeps the added_at it was registered with on the machine it
|
|
131
|
+
# came from, so the same account reads identically across the fleet.
|
|
132
|
+
'added_at': added_at or time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
109
133
|
})
|
|
110
134
|
accounts.sort(key=lambda a: a['id'])
|
|
111
135
|
doc['accounts'] = accounts
|
|
@@ -128,6 +152,17 @@ for a in json.load(open(sys.argv[1])).get('accounts', []):
|
|
|
128
152
|
PYEOF
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
manifest_email_of() { # prints the email registered for <id>, empty if unknown
|
|
156
|
+
[ -f "$MANIFEST" ] || return 0
|
|
157
|
+
"$PYBIN" - "$MANIFEST" "$1" <<'PYEOF' 2>/dev/null
|
|
158
|
+
import json, sys
|
|
159
|
+
for a in json.load(open(sys.argv[1])).get('accounts', []):
|
|
160
|
+
if isinstance(a, dict) and a.get('id') == sys.argv[2]:
|
|
161
|
+
print(a.get('email', ''))
|
|
162
|
+
break
|
|
163
|
+
PYEOF
|
|
164
|
+
}
|
|
165
|
+
|
|
131
166
|
manifest_del_account() { # id
|
|
132
167
|
"$PYBIN" - "$MANIFEST" "$1" <<'PYEOF'
|
|
133
168
|
import json, sys
|
|
@@ -143,8 +178,11 @@ PYEOF
|
|
|
143
178
|
}
|
|
144
179
|
|
|
145
180
|
auto_sync() { # best effort after mutations, Mac only, loud on failure
|
|
146
|
-
[ "$(machine_kind)" = "mac" ] || return 0
|
|
147
181
|
[ "${CLAUDE_MULTIACC_NO_SYNC:-0}" = "1" ] && return 0
|
|
182
|
+
# Local-only pool (target 'none'): there is nothing to push — the panel/runner
|
|
183
|
+
# daemon distributes accounts — so a mutation must not warn about a missing server.
|
|
184
|
+
sync_target_is_local "$(sync_target)" && return 0
|
|
185
|
+
[ "$(machine_kind)" = "mac" ] || return 0
|
|
148
186
|
# A replica pool never pushes (the source machine owns the account set) —
|
|
149
187
|
# silently, so every mutation on a replica does not nag about it.
|
|
150
188
|
sync_is_replica && return 0
|
|
@@ -154,6 +192,14 @@ auto_sync() { # best effort after mutations, Mac only, loud on failure
|
|
|
154
192
|
|
|
155
193
|
cmd_list() {
|
|
156
194
|
require_manifest
|
|
195
|
+
local json=0
|
|
196
|
+
while [ $# -gt 0 ]; do
|
|
197
|
+
case "$1" in
|
|
198
|
+
--json) json=1; shift ;;
|
|
199
|
+
*) die "unknown option: $1 (usage: claude-accounts list [--json])" ;;
|
|
200
|
+
esac
|
|
201
|
+
done
|
|
202
|
+
if [ "$json" = "1" ]; then emit_report_json list; return $?; fi
|
|
157
203
|
"$PYBIN" - "$MANIFEST" "$ACC_ROOT" "$LIB_DIR" "$(machine_kind)" <<'PYEOF'
|
|
158
204
|
import json, os, re, sys
|
|
159
205
|
doc = json.load(open(sys.argv[1]))
|
|
@@ -205,6 +251,14 @@ PYEOF
|
|
|
205
251
|
|
|
206
252
|
cmd_status() {
|
|
207
253
|
require_manifest
|
|
254
|
+
local json=0
|
|
255
|
+
while [ $# -gt 0 ]; do
|
|
256
|
+
case "$1" in
|
|
257
|
+
--json) json=1; shift ;;
|
|
258
|
+
*) die "unknown option: $1 (usage: claude-accounts status [--json])" ;;
|
|
259
|
+
esac
|
|
260
|
+
done
|
|
261
|
+
if [ "$json" = "1" ]; then emit_report_json status; return $?; fi
|
|
208
262
|
"$PYBIN" - "$MANIFEST" "$ACC_ROOT" "$LIB_DIR" "$(machine_kind)" <<'PYEOF'
|
|
209
263
|
import json, os, sys, time
|
|
210
264
|
doc = json.load(open(sys.argv[1]))
|
|
@@ -922,11 +976,12 @@ cmd_relogin() {
|
|
|
922
976
|
|
|
923
977
|
cmd_limits() {
|
|
924
978
|
require_manifest
|
|
925
|
-
local quiet=0 force=0
|
|
979
|
+
local quiet=0 force=0 json=0
|
|
926
980
|
while [ $# -gt 0 ]; do
|
|
927
981
|
case "$1" in
|
|
928
982
|
--quiet) quiet=1; shift ;;
|
|
929
983
|
--force) force=1; shift ;; # ignore freshness/backoff (manual override)
|
|
984
|
+
--json) json=1; quiet=1; shift ;; # refresh silently, then emit the report
|
|
930
985
|
*) die "unknown option: $1" ;;
|
|
931
986
|
esac
|
|
932
987
|
done
|
|
@@ -935,14 +990,20 @@ cmd_limits() {
|
|
|
935
990
|
if ! mkdir "$lock" 2>/dev/null; then
|
|
936
991
|
local age=$(( $(epoch_now) - $(file_mtime "$lock") ))
|
|
937
992
|
if [ "$age" -lt 120 ]; then
|
|
993
|
+
# A --json caller still gets the document (built from the state on disk) —
|
|
994
|
+
# a machine-readable verb must never answer a concurrent run with silence.
|
|
995
|
+
if [ "$json" = "1" ]; then emit_report_json limits; return $?; fi
|
|
938
996
|
[ "$quiet" = "1" ] || echo "another limits refresh is running; skipping"
|
|
939
997
|
return 0
|
|
940
998
|
fi
|
|
941
999
|
rm -rf "$lock"
|
|
942
|
-
mkdir "$lock" 2>/dev/null
|
|
1000
|
+
if ! mkdir "$lock" 2>/dev/null; then
|
|
1001
|
+
if [ "$json" = "1" ]; then emit_report_json limits; return $?; fi
|
|
1002
|
+
return 0
|
|
1003
|
+
fi
|
|
943
1004
|
fi
|
|
944
|
-
|
|
945
|
-
trap
|
|
1005
|
+
LIMITS_LOCK="$lock"
|
|
1006
|
+
trap limits_lock_release EXIT
|
|
946
1007
|
rotate_log limits.log
|
|
947
1008
|
# NB: expired OAuth access tokens are refreshed inside the Python below via the
|
|
948
1009
|
# refresh-token grant. (`claude auth status` was tried for this and does NOT
|
|
@@ -960,7 +1021,7 @@ import json, os, sys, time, urllib.request
|
|
|
960
1021
|
root, threshold, quiet, url, force = sys.argv[1], int(sys.argv[2]), sys.argv[3] == '1', sys.argv[4], sys.argv[5] == '1'
|
|
961
1022
|
now = time.time()
|
|
962
1023
|
# Don't re-fetch an account whose data is younger than this (endpoint rate-limits).
|
|
963
|
-
MIN_FETCH_INTERVAL = int(os.environ.get('CLAUDE_MULTIACC_MIN_FETCH', '
|
|
1024
|
+
MIN_FETCH_INTERVAL = int(os.environ.get('CLAUDE_MULTIACC_MIN_FETCH', '240'))
|
|
964
1025
|
|
|
965
1026
|
# OAuth refresh-token grant — the same endpoint + public client id Claude Code
|
|
966
1027
|
# itself uses to keep .credentials.json alive. An account that sits idle past its
|
|
@@ -1350,13 +1411,21 @@ for acct in manifest.get('accounts', []):
|
|
|
1350
1411
|
say(f"{aid}: LIMITED {worst['name']} at {worst['percent']}% (resets {worst['resets_at']})")
|
|
1351
1412
|
else:
|
|
1352
1413
|
if os.path.exists(mpath):
|
|
1353
|
-
# A shim-written
|
|
1354
|
-
#
|
|
1414
|
+
# A shim-written marker outlives a clean limits pass while its own window
|
|
1415
|
+
# is still open:
|
|
1416
|
+
# error-cooldown — the account failed a real call moments ago.
|
|
1417
|
+
# client-rate-limit — Claude Code itself was REJECTED on this account and
|
|
1418
|
+
# recorded the reset the API handed it. That is
|
|
1419
|
+
# first-hand evidence; a usage payload that disagrees
|
|
1420
|
+
# (different bucket set, cached edge) must not unpark
|
|
1421
|
+
# the account early and send work straight back into
|
|
1422
|
+
# the 429.
|
|
1355
1423
|
keep = False
|
|
1356
1424
|
try:
|
|
1357
1425
|
txt = open(mpath).read()
|
|
1358
1426
|
first = txt.splitlines()[0] if txt else ''
|
|
1359
|
-
if 'reason=error-cooldown' in txt
|
|
1427
|
+
if ('reason=error-cooldown' in txt or 'reason=client-rate-limit' in txt) \
|
|
1428
|
+
and first.isdigit() and int(first) > now:
|
|
1360
1429
|
keep = True
|
|
1361
1430
|
except Exception:
|
|
1362
1431
|
pass
|
|
@@ -1367,6 +1436,18 @@ for acct in manifest.get('accounts', []):
|
|
|
1367
1436
|
detail = ' '.join(f"{b['name']}={b['percent']}%" for b in buckets)
|
|
1368
1437
|
print(f'{aid}: ok {detail}')
|
|
1369
1438
|
PYEOF
|
|
1439
|
+
local refresh_rc=$?
|
|
1440
|
+
# Release the lock BEFORE the report: a --json caller must not hold the refresh
|
|
1441
|
+
# lock while a consumer reads its output.
|
|
1442
|
+
limits_lock_release
|
|
1443
|
+
trap - EXIT
|
|
1444
|
+
# The refresher fails open per account; a NON-zero status means the pass itself
|
|
1445
|
+
# broke (unreadable manifest, dead python). Report the state anyway — stale data
|
|
1446
|
+
# beats silence — but hand the caller the failure, exactly as before --json existed.
|
|
1447
|
+
if [ "$json" = "1" ]; then
|
|
1448
|
+
emit_report_json limits || return $?
|
|
1449
|
+
fi
|
|
1450
|
+
return "$refresh_rc"
|
|
1370
1451
|
}
|
|
1371
1452
|
|
|
1372
1453
|
cmd_verify() {
|
|
@@ -1606,17 +1687,45 @@ sync_is_replica() {
|
|
|
1606
1687
|
|
|
1607
1688
|
cmd_sync() {
|
|
1608
1689
|
require_manifest
|
|
1690
|
+
local allow_empty=0 no_server=0
|
|
1691
|
+
while [ $# -gt 0 ]; do
|
|
1692
|
+
case "$1" in
|
|
1693
|
+
--allow-empty) allow_empty=1; shift ;;
|
|
1694
|
+
--no-server) no_server=1; shift ;; # this run pushes nowhere, whatever the manifest says
|
|
1695
|
+
*) die "unknown option: $1 (usage: claude-accounts sync [--no-server] [--allow-empty])" ;;
|
|
1696
|
+
esac
|
|
1697
|
+
done
|
|
1698
|
+
local server sroot srepo
|
|
1699
|
+
server="$(sync_target)"
|
|
1700
|
+
sroot="$(sync_target_root)"
|
|
1701
|
+
srepo="$(sync_target_repo)"
|
|
1702
|
+
# LOCAL-ONLY: no ssh target at all, because a panel/runner daemon distributes this
|
|
1703
|
+
# pool. Validate + fix up locally and stop — the same verb keeps working, it simply
|
|
1704
|
+
# has nowhere to push. (Not Mac-gated: a local pool is legitimate on any host.)
|
|
1705
|
+
# This mode NARROWS the replica rule, it never widens it: a replica pushes nothing,
|
|
1706
|
+
# and a local-only pool pushes nothing whether or not it is a replica. The marker is
|
|
1707
|
+
# still honored and still reported, so a replica can never start pushing by having
|
|
1708
|
+
# its sync target changed.
|
|
1709
|
+
if [ "$no_server" = "1" ] || sync_target_is_local "$server"; then
|
|
1710
|
+
rotate_log sync.log
|
|
1711
|
+
manifest_well_formed || { log_to sync.log "FAIL: manifest malformed (local sync)"; \
|
|
1712
|
+
die "manifest is not valid JSON or has no well-formed accounts — fix $MANIFEST"; }
|
|
1713
|
+
local role="source"
|
|
1714
|
+
sync_is_replica && role="replica"
|
|
1715
|
+
log_to sync.log "sync (local-only, role=$role): no server target${no_server:+ (--no-server)}"
|
|
1716
|
+
local_pool_fixup
|
|
1717
|
+
if [ "$role" = "replica" ]; then
|
|
1718
|
+
echo "sync ok (local-only, and this pool is a sync replica — nothing pushed either way)"
|
|
1719
|
+
else
|
|
1720
|
+
echo "sync ok (local-only: pool at $ACC_ROOT validated and re-seeded; nothing pushed)"
|
|
1721
|
+
fi
|
|
1722
|
+
return 0
|
|
1723
|
+
fi
|
|
1609
1724
|
[ "$(machine_kind)" = "mac" ] || die "sync runs on the Mac (source of truth), not the server"
|
|
1610
1725
|
if sync_is_replica; then
|
|
1611
1726
|
echo "this pool is a sync replica — the source machine pushes here; nothing sent"
|
|
1612
1727
|
return 0
|
|
1613
1728
|
fi
|
|
1614
|
-
local allow_empty=0
|
|
1615
|
-
[ "${1:-}" = "--allow-empty" ] && allow_empty=1
|
|
1616
|
-
local server sroot srepo
|
|
1617
|
-
server="$(manifest_get server "$DEFAULT_SERVER")"
|
|
1618
|
-
sroot="$(manifest_get server_root "$DEFAULT_SERVER_ROOT")"
|
|
1619
|
-
srepo="$(manifest_get server_repo "$DEFAULT_SERVER_REPO")"
|
|
1620
1729
|
# These land inside remote shell commands — anything but a plain target/path is a
|
|
1621
1730
|
# command-injection vector from a corrupted or hand-edited manifest. EVERY target
|
|
1622
1731
|
# (primary and peers) is validated before anything is pushed anywhere.
|
|
@@ -1648,17 +1757,8 @@ EOF
|
|
|
1648
1757
|
# never be pushed (it would blank the target pools), and must never make the removal
|
|
1649
1758
|
# propagation wipe a target's credentials. Emptying the pool on purpose is possible
|
|
1650
1759
|
# via `sync --allow-empty`, so this can never happen by accident.
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
doc = json.load(open(sys.argv[1]))
|
|
1654
|
-
accounts = doc.get('accounts')
|
|
1655
|
-
if not isinstance(accounts, list):
|
|
1656
|
-
sys.exit(1)
|
|
1657
|
-
for a in accounts:
|
|
1658
|
-
if not isinstance(a, dict) or not re.fullmatch(r'acct-\d{2}', str(a.get('id', ''))) \
|
|
1659
|
-
or not str(a.get('email', '')).strip():
|
|
1660
|
-
sys.exit(1)
|
|
1661
|
-
PYEOF
|
|
1760
|
+
manifest_well_formed \
|
|
1761
|
+
|| fail "manifest is not valid JSON or has no well-formed accounts — refusing to sync"
|
|
1662
1762
|
if [ -z "$(account_ids)" ] && [ "$allow_empty" != "1" ]; then
|
|
1663
1763
|
fail "manifest has zero accounts — refusing to blank the target pools (use 'sync --allow-empty' if that is really intended)"
|
|
1664
1764
|
fi
|
|
@@ -1685,13 +1785,7 @@ EOF
|
|
|
1685
1785
|
|
|
1686
1786
|
cmd_post_sync() {
|
|
1687
1787
|
require_manifest
|
|
1688
|
-
|
|
1689
|
-
for id in $(account_ids); do
|
|
1690
|
-
d="$ACC_ROOT/$id"
|
|
1691
|
-
seed_account_dir "$d"
|
|
1692
|
-
[ -f "$d/server.token" ] && chmod 600 "$d/server.token" 2>/dev/null
|
|
1693
|
-
[ -f "$d/.credentials.json" ] && chmod 600 "$d/.credentials.json" 2>/dev/null
|
|
1694
|
-
done
|
|
1788
|
+
local_pool_fixup
|
|
1695
1789
|
log_to sync.log "post-sync: seeded $(account_ids | wc -l | tr -d ' ') account dirs"
|
|
1696
1790
|
( cmd_limits --quiet ) || true # subshell: release the limits lock before verify
|
|
1697
1791
|
cmd_verify --quick
|
|
@@ -1761,6 +1855,8 @@ case "${1:-help}" in
|
|
|
1761
1855
|
status) shift; cmd_status "$@" ;;
|
|
1762
1856
|
add) shift; cmd_add "$@" ;;
|
|
1763
1857
|
import) shift; cmd_import "$@" ;;
|
|
1858
|
+
export-credential|export-cred) shift; cmd_export_credential "$@" ;;
|
|
1859
|
+
import-credential|import-cred) shift; cmd_import_credential "$@" ;;
|
|
1764
1860
|
adopt) shift; cmd_adopt "$@" ;;
|
|
1765
1861
|
dedupe) shift; cmd_dedupe "$@" ;;
|
|
1766
1862
|
remove) shift; cmd_remove "$@" ;;
|