dsh-agy 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 +174 -0
- package/bin/dsh-agy.js +6 -0
- package/cordis.patch.yml +13 -0
- package/lib/blob-D1e7_uT1.mjs +63 -0
- package/lib/blob-D1e7_uT1.mjs.map +1 -0
- package/lib/cli/index.d.mts +10 -0
- package/lib/cli/index.d.mts.map +1 -0
- package/lib/cli/index.mjs +317 -0
- package/lib/cli/index.mjs.map +1 -0
- package/lib/exchange-MhFUiBAm.mjs +240 -0
- package/lib/exchange-MhFUiBAm.mjs.map +1 -0
- package/lib/import-DnKzgr-0.mjs +187 -0
- package/lib/import-DnKzgr-0.mjs.map +1 -0
- package/lib/index.d.mts +8 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +15 -0
- package/lib/index.mjs.map +1 -0
- package/lib/models-Cu1g7AXV.mjs +222 -0
- package/lib/models-Cu1g7AXV.mjs.map +1 -0
- package/lib/parse-CiEzD9d_.mjs +174 -0
- package/lib/parse-CiEzD9d_.mjs.map +1 -0
- package/lib/plugin-common-I56kmGjr.mjs +270 -0
- package/lib/plugin-common-I56kmGjr.mjs.map +1 -0
- package/lib/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/lib/session-tawi2_uh.mjs +1221 -0
- package/lib/session-tawi2_uh.mjs.map +1 -0
- package/lib/translate-h2bjH7ub.mjs +148 -0
- package/lib/translate-h2bjH7ub.mjs.map +1 -0
- package/lib/web/plugin.d.mts +8 -0
- package/lib/web/plugin.d.mts.map +1 -0
- package/lib/web/plugin.mjs +1618 -0
- package/lib/web/plugin.mjs.map +1 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-agy contributors
|
|
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
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# dsh-agy
|
|
2
|
+
|
|
3
|
+
Google Antigravity (agy) access for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):
|
|
4
|
+
OAuth authentication, a multi-account pool with automatic 429 rotation, device
|
|
5
|
+
fingerprinting, and both CLI and web management.
|
|
6
|
+
|
|
7
|
+
> 中文文档:[docs/README_zh.md](docs/README_zh.md)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **OAuth login**: one-click sign-in via browser OAuth callback, with headless
|
|
12
|
+
paste-URL mode and a remote paste-credential blob channel.
|
|
13
|
+
- **Two management surfaces**: web and CLI, either one works, core features are
|
|
14
|
+
the same.
|
|
15
|
+
- **Multi-account pool**: encrypted account store, automatic rotation on rate
|
|
16
|
+
limits, per-account cooldown with tiered backoff, per-account device
|
|
17
|
+
fingerprints.
|
|
18
|
+
- **Quota dashboard**: only active when DSH Web is running; append `/agy` to
|
|
19
|
+
your dsh web address: login, account management, per-model quota bars, model
|
|
20
|
+
testing, credential export/import, fingerprint management.
|
|
21
|
+
- **CLI**: `dsh-agy login|status|import|verify|logout` works standalone, with or
|
|
22
|
+
without a harness.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
### Path A: DSH Web GUI Users (Recommended — 100% Web UI, zero CLI commands)
|
|
27
|
+
|
|
28
|
+
For users using DeepSeek Harness browser workspace / Web GUI:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# 1. Install plugin into DSH web profile (via dsh CLI, or pnpx/npx if dsh is not in PATH)
|
|
32
|
+
dsh plugin --profile web add dsh-agy
|
|
33
|
+
# or: npx @deepseek-ai/dsh plugin --profile web add dsh-agy
|
|
34
|
+
|
|
35
|
+
# 2. Launch DSH Web
|
|
36
|
+
dsh web
|
|
37
|
+
|
|
38
|
+
# 3. Open dashboard at http://127.0.0.1:3080/agy
|
|
39
|
+
# Click "Login with Google", complete OAuth authorization, and start using the agy provider
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Path B: Headless / Terminal Only (Standalone CLI)
|
|
43
|
+
|
|
44
|
+
For Linux VPS, SSH remote servers, or headless CI environments:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
# Run directly without global install (npx / pnpx)
|
|
48
|
+
npx dsh-agy login
|
|
49
|
+
npx dsh-agy status
|
|
50
|
+
|
|
51
|
+
# Or install globally
|
|
52
|
+
npm install -g dsh-agy
|
|
53
|
+
dsh-agy login # interactive OAuth (browser, --headless paste, or --blob)
|
|
54
|
+
dsh-agy status # list accounts + quota summary
|
|
55
|
+
dsh-agy verify # refresh + health check
|
|
56
|
+
dsh-agy import <file> # import agy auth.json or credential blob (--blob)
|
|
57
|
+
dsh-agy logout # remove account
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Path C: Local Development & Link
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
git clone https://github.com/chaos-03x/dsh-agy.git
|
|
64
|
+
cd dsh-agy && pnpm install && pnpm run build
|
|
65
|
+
dsh plugin --profile web link .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires Node >= 20.
|
|
69
|
+
|
|
70
|
+
## Uninstall
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
# 1. Remove the DSH plugin from a profile
|
|
74
|
+
dsh plugin --profile web remove dsh-agy
|
|
75
|
+
|
|
76
|
+
# 2. Uninstall the CLI
|
|
77
|
+
npm uninstall -g dsh-agy
|
|
78
|
+
|
|
79
|
+
# 3. Optional: delete local account data (accounts + master key + fingerprint override)
|
|
80
|
+
dsh-agy logout # remove accounts first (or skip)
|
|
81
|
+
rm -f ~/.dsh/agy-accounts.json
|
|
82
|
+
# remove only the AGY_MASTER_KEY line from ~/.dsh/.credentials.yaml — keep other keys!
|
|
83
|
+
rm -f ~/.dsh/agy-fingerprint-data.json # only if you created an override
|
|
84
|
+
|
|
85
|
+
# 4. Optional: revoke the Google-side authorization
|
|
86
|
+
# Google account security → Third-party access → revoke "Antigravity"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Deleting local files does **not** revoke Google-side tokens; the refresh token stays
|
|
90
|
+
valid until it expires or you revoke it in your Google account security settings.
|
|
91
|
+
|
|
92
|
+
## Other things you may care about
|
|
93
|
+
|
|
94
|
+
### Rotation mechanics
|
|
95
|
+
|
|
96
|
+
429 (Too Many Requests) responses:
|
|
97
|
+
|
|
98
|
+
| Category | Behavior |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `soft_rate_limit` (Retry-After < 3s) | immediate retry on the same account, no cooldown |
|
|
101
|
+
| `rate_limited` | 5-minute cooldown + switch to the next account (same account when single) |
|
|
102
|
+
| `quota_exhausted` ("quota reached", "individual quota", RESOURCE_EXHAUSTED…) | 24-hour cooldown — no further calls to that account for the day |
|
|
103
|
+
| `unknown` | exponential backoff |
|
|
104
|
+
|
|
105
|
+
401/403 → account revoked (marked for re-authentication). Success resets the failure
|
|
106
|
+
counter.
|
|
107
|
+
|
|
108
|
+
### About cache hits: why not 99% like DeepSeek V4?
|
|
109
|
+
|
|
110
|
+
Bottom line: the cache hit strategy is decided by the model provider's cache
|
|
111
|
+
mechanics (for us, Antigravity's); agy's mechanics differ from DeepSeek's in
|
|
112
|
+
two ways, so its hit rate is naturally a notch below DeepSeek's.
|
|
113
|
+
|
|
114
|
+
**First, the entry threshold.** DeepSeek's caching is on by default with no
|
|
115
|
+
threshold — its very first request already hits a previously cached system
|
|
116
|
+
prompt. agy's Gemini-tiered models only start caching once the request prefix
|
|
117
|
+
reaches roughly 16k tokens, while DSH's default bare system prompt is only
|
|
118
|
+
about 13k — below the line. So every new conversation's first 1-2 requests
|
|
119
|
+
are 0%, until the accumulated messages pass 16k.
|
|
120
|
+
|
|
121
|
+
**Second, how fast the cache updates.** DeepSeek refreshes its cache at the
|
|
122
|
+
end of every request — only the newest message misses each round, giving
|
|
123
|
+
near-100%. agy's cache updates lag: this round's additions are not hit in
|
|
124
|
+
the next round — they enter the cache roughly two rounds later, and requests
|
|
125
|
+
for the same content in between all count as misses. Every round wastes
|
|
126
|
+
about 1.5-2× its additions; the long-conversation hit rate keeps rising as
|
|
127
|
+
the context grows, bounded by the model's context window.
|
|
128
|
+
|
|
129
|
+
**Practical tips**
|
|
130
|
+
|
|
131
|
+
- Don't expect 99% from agy: the gap comes from upstream mechanics, with no
|
|
132
|
+
room to optimize.
|
|
133
|
+
- If you have a weird number obsession, stuff some custom content into the
|
|
134
|
+
System Prompt (MCP / tool definitions / roleplay ...).
|
|
135
|
+
|
|
136
|
+
### Storage & secrets
|
|
137
|
+
|
|
138
|
+
- Accounts: `~/.dsh/agy-accounts.json` — AES-256-GCM encrypted; the master key lives
|
|
139
|
+
in `~/.dsh/.credentials.yaml` (`AGY_MASTER_KEY`, 0600). `$DSH_HOME` relocates both.
|
|
140
|
+
- Fingerprint pools (version strings, SDK clients) are user-overridable via
|
|
141
|
+
`~/.dsh/agy-fingerprint-data.json` — no code release needed to keep them current.
|
|
142
|
+
|
|
143
|
+
## ⚠️ Disclaimer
|
|
144
|
+
|
|
145
|
+
This plugin authenticates with Google's consumer OAuth client that ships with the
|
|
146
|
+
Antigravity desktop product and uses the Antigravity Cloud Code API outside of that
|
|
147
|
+
product. This may violate Antigravity's terms of service. **Use at your own risk** —
|
|
148
|
+
accounts can be rate-limited, throttled, or banned. Multi-account rotation, device
|
|
149
|
+
fingerprinting, and the signature-bypass sentinel are enabled by default and are
|
|
150
|
+
designed to work around upstream limits; you are responsible for how you use them and
|
|
151
|
+
for any account consequences.
|
|
152
|
+
|
|
153
|
+
## Credits
|
|
154
|
+
|
|
155
|
+
This project references logic and data from the following MIT-licensed sources:
|
|
156
|
+
|
|
157
|
+
| Source | Content |
|
|
158
|
+
|---|---|
|
|
159
|
+
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) (archived) | OAuth flow shape, account-store schema & versioned migration, 429/backoff concepts, fingerprint design |
|
|
160
|
+
| [antigravity-claude-proxy PR #170](https://github.com/badrisnarayanan/antigravity-claude-proxy/pull/170) | Device fingerprint generation (via opencode-antigravity-auth) |
|
|
161
|
+
| [OmniRoute](https://github.com/diegosouzapw/OmniRoute) | Wire format (envelope, headers, SSE), endpoint order, `agy` token-file parsing, paste-credential blob codec, thoughtSignature replay, 429 category engine |
|
|
162
|
+
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | Plugin shell, `LlmAdapter` seam, DSH conventions |
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
pnpm install
|
|
168
|
+
pnpm test # vitest, fixture-driven, no network
|
|
169
|
+
pnpm run record:fixtures # re-record real-API fixtures (needs a real account)
|
|
170
|
+
pnpm run e2e # real-account end-to-end (needs AGY_REFRESH_TOKEN)
|
|
171
|
+
pnpm run debug:request # endpoint/header bisection probe
|
|
172
|
+
pnpm run verify:tools # live two-turn tool-signature check
|
|
173
|
+
npm pack --dry-run # verify the publishable artifact
|
|
174
|
+
```
|
package/bin/dsh-agy.js
ADDED
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# dsh-agy bundle patch: mounts the dsh-agy plugins into a dsh profile.
|
|
2
|
+
|
|
3
|
+
# Install with: dsh plugin --profile <name> add dsh-agy
|
|
4
|
+
# The loader inserts these rows; the main plugin registers the agy LLM adapter
|
|
5
|
+
# routes, the web entry registers the /agy UI once ctx.webServer activates
|
|
6
|
+
# (it stays pending in profiles without a web server).
|
|
7
|
+
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-agy
|
|
10
|
+
name: 'dsh-agy'
|
|
11
|
+
|
|
12
|
+
- id: dsh-agy-web
|
|
13
|
+
name: 'dsh-agy/web'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
//#region src/oauth/blob.ts
|
|
3
|
+
var blob_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
+
CREDENTIAL_BLOB_PREFIX: () => CREDENTIAL_BLOB_PREFIX,
|
|
5
|
+
CREDENTIAL_BLOB_PROVIDER: () => "agy",
|
|
6
|
+
decodeCredentialBlob: () => decodeCredentialBlob,
|
|
7
|
+
encodeCredentialBlob: () => encodeCredentialBlob
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Paste-safe credential blob codec for remote login.
|
|
11
|
+
*
|
|
12
|
+
* Google's `firstparty/nativeapp` consent for this embedded client only releases
|
|
13
|
+
* the authorization code when the loopback redirect is reachable, which never
|
|
14
|
+
* happens on a remote host. A local helper (the `dsh-agy login --remote` path)
|
|
15
|
+
* runs the OAuth on the user's own machine, encodes the raw token response into
|
|
16
|
+
* a single-line blob with this codec, and the remote harness decodes it.
|
|
17
|
+
*
|
|
18
|
+
* Format: `dsh-agy-cred-v1.` + base64url(JSON{v, provider, tokens}) — URL/shell
|
|
19
|
+
* safe, survives copy-paste through terminals.
|
|
20
|
+
*/
|
|
21
|
+
const CREDENTIAL_BLOB_PREFIX = "dsh-agy-cred-v1.";
|
|
22
|
+
const CREDENTIAL_BLOB_VERSION = 1;
|
|
23
|
+
/** Encode a provider + raw OAuth token response into a single-line blob. */
|
|
24
|
+
function encodeCredentialBlob(provider, tokens) {
|
|
25
|
+
if (!provider || !provider.trim()) throw new Error("encodeCredentialBlob: a non-empty provider is required");
|
|
26
|
+
if (!tokens || typeof tokens !== "object" || !tokens.access_token) throw new Error("encodeCredentialBlob: tokens with an access_token are required");
|
|
27
|
+
const payload = {
|
|
28
|
+
v: CREDENTIAL_BLOB_VERSION,
|
|
29
|
+
provider: provider.trim(),
|
|
30
|
+
tokens
|
|
31
|
+
};
|
|
32
|
+
const json = JSON.stringify(payload);
|
|
33
|
+
return `${CREDENTIAL_BLOB_PREFIX}${Buffer.from(json, "utf8").toString("base64url")}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decode and validate a credential blob. Throws on wrong prefix, malformed
|
|
37
|
+
* payload, version mismatch, provider mismatch, or missing access_token.
|
|
38
|
+
*/
|
|
39
|
+
function decodeCredentialBlob(blob) {
|
|
40
|
+
if (typeof blob !== "string" || !blob.startsWith("dsh-agy-cred-v1.")) throw new Error(`decodeCredentialBlob: invalid format — must start with "${CREDENTIAL_BLOB_PREFIX}"`);
|
|
41
|
+
const b64 = blob.slice(16).trim();
|
|
42
|
+
if (!/^[A-Za-z0-9_-]+$/.test(b64)) throw new Error("decodeCredentialBlob: invalid payload — not base64url");
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(Buffer.from(b64, "base64url").toString("utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error("decodeCredentialBlob: payload is not valid JSON");
|
|
48
|
+
}
|
|
49
|
+
if (!parsed || typeof parsed !== "object") throw new Error("decodeCredentialBlob: payload is not an object");
|
|
50
|
+
if (parsed.v !== CREDENTIAL_BLOB_VERSION) throw new Error(`decodeCredentialBlob: unsupported blob version ${String(parsed.v)}`);
|
|
51
|
+
if (parsed.provider !== "agy") throw new Error(`decodeCredentialBlob: provider mismatch — blob is for "${String(parsed.provider)}" but this plugin serves "agy"`);
|
|
52
|
+
const tokens = parsed.tokens;
|
|
53
|
+
if (!tokens || typeof tokens !== "object") throw new Error("decodeCredentialBlob: tokens are missing");
|
|
54
|
+
if (typeof tokens.access_token !== "string") throw new Error("decodeCredentialBlob: access_token is missing");
|
|
55
|
+
return {
|
|
56
|
+
provider: parsed.provider,
|
|
57
|
+
tokens
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { decodeCredentialBlob as n, encodeCredentialBlob as r, blob_exports as t };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=blob-D1e7_uT1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-D1e7_uT1.mjs","names":[],"sources":["../src/oauth/blob.ts"],"sourcesContent":["/**\n * Paste-safe credential blob codec for remote login.\n *\n * Google's `firstparty/nativeapp` consent for this embedded client only releases\n * the authorization code when the loopback redirect is reachable, which never\n * happens on a remote host. A local helper (the `dsh-agy login --remote` path)\n * runs the OAuth on the user's own machine, encodes the raw token response into\n * a single-line blob with this codec, and the remote harness decodes it.\n *\n * Format: `dsh-agy-cred-v1.` + base64url(JSON{v, provider, tokens}) — URL/shell\n * safe, survives copy-paste through terminals.\n */\n\nexport const CREDENTIAL_BLOB_PREFIX = 'dsh-agy-cred-v1.'\n\nconst CREDENTIAL_BLOB_VERSION = 1\n\n/** Provider this blob codec accepts (bound at decode time to prevent replay across providers). */\nexport const CREDENTIAL_BLOB_PROVIDER = 'agy'\n\nexport interface CredentialBlobTokens {\n access_token?: string\n refresh_token?: string\n id_token?: string\n expires_in?: number\n scope?: string\n [key: string]: unknown\n}\n\nexport interface CredentialBlob {\n provider: string\n tokens: CredentialBlobTokens\n}\n\n/** Encode a provider + raw OAuth token response into a single-line blob. */\nexport function encodeCredentialBlob(provider: string, tokens: CredentialBlobTokens): string {\n if (!provider || !provider.trim()) {\n throw new Error('encodeCredentialBlob: a non-empty provider is required')\n }\n if (!tokens || typeof tokens !== 'object' || !tokens.access_token) {\n throw new Error('encodeCredentialBlob: tokens with an access_token are required')\n }\n const payload = { v: CREDENTIAL_BLOB_VERSION, provider: provider.trim(), tokens }\n const json = JSON.stringify(payload)\n return `${CREDENTIAL_BLOB_PREFIX}${Buffer.from(json, 'utf8').toString('base64url')}`\n}\n\n/**\n * Decode and validate a credential blob. Throws on wrong prefix, malformed\n * payload, version mismatch, provider mismatch, or missing access_token.\n */\nexport function decodeCredentialBlob(blob: string): CredentialBlob {\n if (typeof blob !== 'string' || !blob.startsWith(CREDENTIAL_BLOB_PREFIX)) {\n throw new Error(`decodeCredentialBlob: invalid format — must start with \"${CREDENTIAL_BLOB_PREFIX}\"`)\n }\n const b64 = blob.slice(CREDENTIAL_BLOB_PREFIX.length).trim()\n if (!/^[A-Za-z0-9_-]+$/.test(b64)) {\n throw new Error('decodeCredentialBlob: invalid payload — not base64url')\n }\n\n let parsed: { v?: unknown; provider?: unknown; tokens?: unknown }\n try {\n parsed = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8')) as typeof parsed\n } catch {\n throw new Error('decodeCredentialBlob: payload is not valid JSON')\n }\n\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('decodeCredentialBlob: payload is not an object')\n }\n if (parsed.v !== CREDENTIAL_BLOB_VERSION) {\n throw new Error(`decodeCredentialBlob: unsupported blob version ${String(parsed.v)}`)\n }\n if (parsed.provider !== CREDENTIAL_BLOB_PROVIDER) {\n throw new Error(\n `decodeCredentialBlob: provider mismatch — blob is for \"${String(parsed.provider)}\" but this plugin serves \"${CREDENTIAL_BLOB_PROVIDER}\"`,\n )\n }\n const tokens = parsed.tokens\n if (!tokens || typeof tokens !== 'object') {\n throw new Error('decodeCredentialBlob: tokens are missing')\n }\n if (typeof (tokens as CredentialBlobTokens).access_token !== 'string') {\n throw new Error('decodeCredentialBlob: access_token is missing')\n }\n\n return { provider: parsed.provider, tokens: tokens as CredentialBlobTokens }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAaA,MAAa,yBAAyB;AAEtC,MAAM,0BAA0B;;AAoBhC,SAAgB,qBAAqB,UAAkB,QAAsC;CAC3F,IAAI,CAAC,YAAY,CAAC,SAAS,KAAK,GAC9B,MAAM,IAAI,MAAM,wDAAwD;CAE1E,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,CAAC,OAAO,cACnD,MAAM,IAAI,MAAM,gEAAgE;CAElF,MAAM,UAAU;EAAE,GAAG;EAAyB,UAAU,SAAS,KAAK;EAAG;CAAO;CAChF,MAAM,OAAO,KAAK,UAAU,OAAO;CACnC,OAAO,GAAG,yBAAyB,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,SAAS,WAAW;AACnF;;;;;AAMA,SAAgB,qBAAqB,MAA8B;CACjE,IAAI,OAAO,SAAS,YAAY,CAAC,KAAK,WAAA,kBAAiC,GACrE,MAAM,IAAI,MAAM,2DAA2D,uBAAuB,EAAE;CAEtG,MAAM,MAAM,KAAK,MAAM,EAA6B,CAAC,CAAC,KAAK;CAC3D,IAAI,CAAC,mBAAmB,KAAK,GAAG,GAC9B,MAAM,IAAI,MAAM,uDAAuD;CAGzE,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO,KAAK,KAAK,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;CACpE,QAAQ;EACN,MAAM,IAAI,MAAM,iDAAiD;CACnE;CAEA,IAAI,CAAC,UAAU,OAAO,WAAW,UAC/B,MAAM,IAAI,MAAM,gDAAgD;CAElE,IAAI,OAAO,MAAM,yBACf,MAAM,IAAI,MAAM,kDAAkD,OAAO,OAAO,CAAC,GAAG;CAEtF,IAAI,OAAO,aAAA,OACT,MAAM,IAAI,MACR,0DAA0D,OAAO,OAAO,QAAQ,EAAE,+BACpF;CAEF,MAAM,SAAS,OAAO;CACtB,IAAI,CAAC,UAAU,OAAO,WAAW,UAC/B,MAAM,IAAI,MAAM,0CAA0C;CAE5D,IAAI,OAAQ,OAAgC,iBAAiB,UAC3D,MAAM,IAAI,MAAM,+CAA+C;CAGjE,OAAO;EAAE,UAAU,OAAO;EAAkB;CAA+B;AAC7E"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
//#region src/oauth/constants.d.ts
|
|
3
|
+
/** Default loopback callback used by the standalone CLI listener (fixed port, like opencode). */
|
|
4
|
+
declare const AGY_DEFAULT_REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/cli/index.d.ts
|
|
7
|
+
declare function createProgram(): Command;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { AGY_DEFAULT_REDIRECT_URI, createProgram };
|
|
10
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/oauth/constants.ts","../../src/cli/index.ts"],"mappings":";;;cA6Ba;;;iBC8NG,iBAAiB"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { c as loadMasterKey, d as resolveMasterKeyCodec, i as JsonAccountStore, o as createAesGcmCodec, s as deriveKey, t as AgySessionManager, u as resolveDshHome, y as AGY_DEFAULT_REDIRECT_URI } from "../session-tawi2_uh.mjs";
|
|
2
|
+
import { t as exchangeAntigravity } from "../exchange-MhFUiBAm.mjs";
|
|
3
|
+
import { n as upsertImportedAccount, r as authorizeAntigravity, t as parseImportSource } from "../import-DnKzgr-0.mjs";
|
|
4
|
+
import { r as encodeCredentialBlob } from "../blob-D1e7_uT1.mjs";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
//#region src/cli/callback-server.ts
|
|
11
|
+
/** Loopback OAuth callback server for the standalone CLI login flow. */
|
|
12
|
+
const SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>
|
|
13
|
+
<body style="font-family:system-ui;text-align:center;padding:3rem">
|
|
14
|
+
<h2>✓ Authentication successful</h2>
|
|
15
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
16
|
+
</body></html>`;
|
|
17
|
+
/** Listen on 127.0.0.1:<port>/oauth-callback and resolve with the code+state. */
|
|
18
|
+
function startCallbackServer(options = {}) {
|
|
19
|
+
const port = options.port ?? 51121;
|
|
20
|
+
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
21
|
+
let resolveResult;
|
|
22
|
+
let rejectResult;
|
|
23
|
+
const result = new Promise((resolve, reject) => {
|
|
24
|
+
resolveResult = resolve;
|
|
25
|
+
rejectResult = reject;
|
|
26
|
+
});
|
|
27
|
+
let resolveReady;
|
|
28
|
+
let rejectReady;
|
|
29
|
+
const ready = new Promise((resolve, reject) => {
|
|
30
|
+
resolveReady = resolve;
|
|
31
|
+
rejectReady = reject;
|
|
32
|
+
});
|
|
33
|
+
const server = createServer((req, res) => {
|
|
34
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
35
|
+
if (url.pathname === "/oauth-callback") {
|
|
36
|
+
const code = url.searchParams.get("code");
|
|
37
|
+
const state = url.searchParams.get("state");
|
|
38
|
+
if (!code || !state) {
|
|
39
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
40
|
+
res.end("<h2>Missing code or state</h2>");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.writeHead(200, {
|
|
44
|
+
"Content-Type": "text/html",
|
|
45
|
+
connection: "close"
|
|
46
|
+
});
|
|
47
|
+
res.end(SUCCESS_HTML);
|
|
48
|
+
resolveResult({
|
|
49
|
+
code,
|
|
50
|
+
state,
|
|
51
|
+
url: url.toString()
|
|
52
|
+
});
|
|
53
|
+
setTimeout(() => server.close(), 1500);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(404);
|
|
57
|
+
res.end("Not found");
|
|
58
|
+
});
|
|
59
|
+
server.on("error", (error) => {
|
|
60
|
+
if (error.code === "EADDRINUSE") rejectResult(/* @__PURE__ */ new Error(`Port ${port} is already in use — pass --port to pick another.`));
|
|
61
|
+
else rejectResult(error);
|
|
62
|
+
rejectReady(error);
|
|
63
|
+
});
|
|
64
|
+
server.listen(port, "127.0.0.1", () => {
|
|
65
|
+
resolveReady();
|
|
66
|
+
});
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
rejectResult(/* @__PURE__ */ new Error("Authentication timed out — no callback received."));
|
|
69
|
+
server.close();
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
return {
|
|
72
|
+
result,
|
|
73
|
+
ready,
|
|
74
|
+
async close() {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
if (server.listening) {
|
|
77
|
+
server.closeAllConnections();
|
|
78
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Open a URL in the system browser (best effort). */
|
|
84
|
+
async function openBrowser(url) {
|
|
85
|
+
const { spawn } = await import("node:child_process");
|
|
86
|
+
const platform = process.platform;
|
|
87
|
+
const command = platform === "darwin" ? ["open", url] : platform === "win32" ? [
|
|
88
|
+
"cmd",
|
|
89
|
+
"/c",
|
|
90
|
+
"start",
|
|
91
|
+
"",
|
|
92
|
+
url
|
|
93
|
+
] : ["xdg-open", url];
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const child = spawn(command[0], command.slice(1), { stdio: "ignore" });
|
|
96
|
+
child.on("error", () => resolve(false));
|
|
97
|
+
child.on("spawn", () => resolve(true));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/cli/index.ts
|
|
102
|
+
/**
|
|
103
|
+
* dsh-agy CLI: login / status / import / verify / logout.
|
|
104
|
+
* Standalone bin — the dsh launcher has no plugin subcommand slot, and the
|
|
105
|
+
* remote paste-blob flow must run on machines without a harness.
|
|
106
|
+
*/
|
|
107
|
+
/** Package version, read from the shipped package.json — never hard-coded twice. */
|
|
108
|
+
const { version: PACKAGE_VERSION } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
109
|
+
function createStore(options = {}) {
|
|
110
|
+
const dshHome = resolveDshHome();
|
|
111
|
+
let codec;
|
|
112
|
+
if (options.readOnly) {
|
|
113
|
+
const masterKey = loadMasterKey(dshHome);
|
|
114
|
+
if (!masterKey) throw new Error("No agy account store found — run `dsh-agy login` first.");
|
|
115
|
+
codec = createAesGcmCodec(deriveKey(masterKey));
|
|
116
|
+
} else codec = resolveMasterKeyCodec(dshHome).codec;
|
|
117
|
+
return new JsonAccountStore({
|
|
118
|
+
file: `${dshHome}/agy-accounts.json`,
|
|
119
|
+
codec
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Read-only store with a friendly error when no credentials exist yet. */
|
|
123
|
+
function createReadOnlyStoreOrExit() {
|
|
124
|
+
try {
|
|
125
|
+
return createStore({ readOnly: true });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function ask(question) {
|
|
132
|
+
const rl = createInterface({
|
|
133
|
+
input: stdin,
|
|
134
|
+
output: stdout
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
return (await rl.question(question)).trim();
|
|
138
|
+
} finally {
|
|
139
|
+
rl.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Remote/SSH sessions cannot reach a local browser; auto-select headless paste. */
|
|
143
|
+
function isRemoteSession() {
|
|
144
|
+
return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY);
|
|
145
|
+
}
|
|
146
|
+
async function loginCommand(options) {
|
|
147
|
+
const store = createStore();
|
|
148
|
+
const redirectUri = `http://localhost:${options.port}/oauth-callback`;
|
|
149
|
+
if (!options.headless && isRemoteSession()) {
|
|
150
|
+
console.log("(SSH session detected — using headless paste flow; pass --headless explicitly to force)");
|
|
151
|
+
options.headless = true;
|
|
152
|
+
}
|
|
153
|
+
let callback;
|
|
154
|
+
if (!options.headless) {
|
|
155
|
+
callback = startCallbackServer({
|
|
156
|
+
port: options.port,
|
|
157
|
+
timeoutMs: Number(options.timeout) || 3e5
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
await callback.ready;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const { url, verifier } = await authorizeAntigravity(redirectUri, options.project ?? "");
|
|
167
|
+
console.log(`\nOpen this URL in a browser to authorize:\n\n ${url}\n`);
|
|
168
|
+
let code;
|
|
169
|
+
let state;
|
|
170
|
+
if (options.headless) {
|
|
171
|
+
const pasted = await ask("After approving, paste the full redirected URL here: ");
|
|
172
|
+
const parsed = new URL(pasted);
|
|
173
|
+
code = parsed.searchParams.get("code") ?? "";
|
|
174
|
+
state = parsed.searchParams.get("state") ?? "";
|
|
175
|
+
if (!code || !state) {
|
|
176
|
+
console.error("Error: pasted URL is missing code or state.");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (!await openBrowser(url)) console.log("(Could not open a browser automatically — open the URL manually.)");
|
|
181
|
+
let callbackResult;
|
|
182
|
+
try {
|
|
183
|
+
callbackResult = await callback.result;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
code = callbackResult.code;
|
|
189
|
+
state = callbackResult.state;
|
|
190
|
+
}
|
|
191
|
+
const result = await exchangeAntigravity(code, state, redirectUri, verifier);
|
|
192
|
+
if (result.type === "failed") {
|
|
193
|
+
console.error(`Login failed: ${result.error}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (options.blob) {
|
|
197
|
+
const blob = encodeCredentialBlob("agy", {
|
|
198
|
+
access_token: result.access,
|
|
199
|
+
refresh_token: result.refresh.split("|")[0],
|
|
200
|
+
expires_in: Math.max(0, Math.round((result.expires - Date.now()) / 1e3))
|
|
201
|
+
});
|
|
202
|
+
console.log(`\nPaste this blob into the remote dashboard/CLI:\n\n${blob}\n`);
|
|
203
|
+
} else {
|
|
204
|
+
const { account, created } = await upsertImportedAccount(store, {
|
|
205
|
+
accessToken: result.access,
|
|
206
|
+
refreshToken: result.refresh.split("|")[0],
|
|
207
|
+
tokenType: "Bearer",
|
|
208
|
+
expiresAt: new Date(result.expires).toISOString(),
|
|
209
|
+
authMethod: "oauth",
|
|
210
|
+
email: result.email ?? null,
|
|
211
|
+
projectId: result.projectId || null
|
|
212
|
+
}, { overwriteExisting: true });
|
|
213
|
+
console.log(`${created ? "Added" : "Updated"} account: ${account.email ?? "(no email)"} (project: ${result.projectId || "default"})`);
|
|
214
|
+
}
|
|
215
|
+
await callback?.close();
|
|
216
|
+
}
|
|
217
|
+
async function statusCommand() {
|
|
218
|
+
const store = createReadOnlyStoreOrExit();
|
|
219
|
+
const storage = await store.load();
|
|
220
|
+
if (storage.accounts.length === 0) {
|
|
221
|
+
console.log("No agy accounts. Run `dsh-agy login` first.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`\n${storage.accounts.length} account(s), active index ${storage.activeIndex}:\n`);
|
|
225
|
+
const sessions = new AgySessionManager({ store });
|
|
226
|
+
for (const [index, account] of storage.accounts.entries()) {
|
|
227
|
+
const marker = index === storage.activeIndex ? "★" : " ";
|
|
228
|
+
const state = account.enabled === false ? "disabled" : account.verificationRequired ? "verification-required" : account.coolingDownUntil && account.coolingDownUntil > Date.now() ? "cooling" : "active";
|
|
229
|
+
console.log(` ${marker} [${index}] ${account.email ?? "(no email)"} — ${state}${account.projectId ? ` (project: ${account.projectId})` : ""}`);
|
|
230
|
+
const session = await sessions.getSession().catch(() => void 0);
|
|
231
|
+
if (session && session.index === index) try {
|
|
232
|
+
const { fetchAvailableModels } = await import("../models-Cu1g7AXV.mjs").then((n) => n.n);
|
|
233
|
+
const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId);
|
|
234
|
+
const entries = Object.entries(discovered.models ?? {});
|
|
235
|
+
if (entries.length > 0) {
|
|
236
|
+
const withQuota = entries.map(([id, entry]) => ({
|
|
237
|
+
id,
|
|
238
|
+
...entry.quotaInfo
|
|
239
|
+
})).filter((e) => typeof e.remainingFraction === "number").sort((a, b) => (a.remainingFraction ?? 0) - (b.remainingFraction ?? 0));
|
|
240
|
+
if (withQuota.length > 0) {
|
|
241
|
+
const lowest = withQuota[0];
|
|
242
|
+
console.log(` models: ${entries.length}, lowest quota: ${Math.round((lowest.remainingFraction ?? 0) * 100)}% (${lowest.id})${lowest.resetTime ? `, resets ${new Date(lowest.resetTime).toISOString()}` : ""}`);
|
|
243
|
+
} else console.log(` models: ${entries.length} (no per-model quota reported)`);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.log(` quota: unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function importCommand(options) {
|
|
251
|
+
const store = createStore();
|
|
252
|
+
let source;
|
|
253
|
+
if (options.file) source = JSON.parse(readFileSync(options.file, "utf8"));
|
|
254
|
+
else source = await ask("Paste the agy token JSON (or blob with --blob): ");
|
|
255
|
+
const kind = options.blob ? "blob" : "json";
|
|
256
|
+
const enriched = await parseImportSource(source, kind);
|
|
257
|
+
const { account, created } = await upsertImportedAccount(store, enriched, {
|
|
258
|
+
email: options.email,
|
|
259
|
+
overwriteExisting: options.overwrite
|
|
260
|
+
});
|
|
261
|
+
console.log(`${created ? "Imported" : "Replaced"} account: ${account.email ?? "(no email)"}${enriched.projectId ? ` (project: ${enriched.projectId})` : ""}`);
|
|
262
|
+
}
|
|
263
|
+
async function verifyCommand(options) {
|
|
264
|
+
const store = createReadOnlyStoreOrExit();
|
|
265
|
+
const sessions = new AgySessionManager({ store });
|
|
266
|
+
const storage = await store.load();
|
|
267
|
+
const indices = options.index !== void 0 ? [Number(options.index)] : storage.accounts.map((_, i) => i);
|
|
268
|
+
for (const index of indices) {
|
|
269
|
+
const account = storage.accounts[index];
|
|
270
|
+
if (!account) {
|
|
271
|
+
console.log(`[${index}] not found`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const result = await sessions.verifyAccount(index);
|
|
275
|
+
if (result.ok) console.log(`[${index}] ${account.email ?? ""} — OK${result.email && result.email !== account.email ? ` (userinfo: ${result.email})` : ""}`);
|
|
276
|
+
else console.log(`[${index}] ${account.email ?? ""} — FAILED: ${result.error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function logoutCommand(options) {
|
|
280
|
+
await createReadOnlyStoreOrExit().mutate((storage) => {
|
|
281
|
+
const index = options.index !== void 0 ? Number(options.index) : options.email ? storage.accounts.findIndex((a) => a.email?.toLowerCase() === options.email.toLowerCase()) : storage.activeIndex;
|
|
282
|
+
if (index < 0 || index >= storage.accounts.length) throw new Error(`account not found (index ${index})`);
|
|
283
|
+
const [removed] = storage.accounts.splice(index, 1);
|
|
284
|
+
if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0;
|
|
285
|
+
console.log(`Removed account: ${removed?.email ?? `[${index}]`}`);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function createProgram() {
|
|
289
|
+
const program = new Command();
|
|
290
|
+
program.name("dsh-agy").description("Google Antigravity (agy) account management for DeepSeek Harness").version(PACKAGE_VERSION);
|
|
291
|
+
program.command("login").description("OAuth login (browser, or headless paste)").option("--headless", "print the URL and wait for a pasted redirect URL", false).option("--blob", "print a paste-credential blob instead of storing the account", false).option("--port <n>", "loopback callback port", "51121").option("--project <id>", "bind the login to a specific project").option("--timeout <ms>", "callback timeout", "300000").action(async (options) => {
|
|
292
|
+
await loginCommand({
|
|
293
|
+
...options,
|
|
294
|
+
timeout: options.timeout
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
program.command("status").description("List accounts and their health").action(async () => {
|
|
298
|
+
await statusCommand();
|
|
299
|
+
});
|
|
300
|
+
program.command("import").description("Import an agy token file or paste credentials").argument("[file]", "path to an agy auth.json token file").option("--blob", "the pasted value is a credential blob", false).option("--email <email>", "account email (skips userinfo verification)").option("--overwrite", "replace an existing account with the same email", false).action(async (file, options) => {
|
|
301
|
+
await importCommand({
|
|
302
|
+
...options,
|
|
303
|
+
file
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
program.command("verify").description("Verify account credentials (refresh + userinfo)").option("--index <n>", "verify one account by index; default all").action(async (options) => {
|
|
307
|
+
await verifyCommand(options);
|
|
308
|
+
});
|
|
309
|
+
program.command("logout").description("Remove an account").option("--index <n>", "account index (default: active)").option("--email <email>", "account email").action(async (options) => {
|
|
310
|
+
await logoutCommand(options);
|
|
311
|
+
});
|
|
312
|
+
return program;
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
export { AGY_DEFAULT_REDIRECT_URI, createProgram };
|
|
316
|
+
|
|
317
|
+
//# sourceMappingURL=index.mjs.map
|