vibemancer 1.0.37 → 1.0.39

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 CHANGED
@@ -16,11 +16,41 @@ vibemancer bots List built-in bots
16
16
  vibemancer bots --mine List YOUR uploaded bots, retired ones included
17
17
  vibemancer build Compile to a standalone bundle
18
18
  vibemancer upload Upload to vibemancer.com for rated competition
19
- vibemancer pull Download your bot's source back from the server
19
+ vibemancer pull Download your bot's source back from the server.
20
+ REFUSES if the local file has changes it did not write.
20
21
  vibemancer pull --name Foo ...by name, with no local file needed
22
+ vibemancer pull --force ...overwriting local changes anyway
21
23
  vibemancer login Sign in (required before upload/pull)
24
+ vibemancer missile-calc Compare missile builds without running a fight
22
25
  ```
23
26
 
27
+ `vibemancer --help` lists every flag a command accepts, and a flag a command does not accept is
28
+ refused by name with the closest real one suggested - an unrecognised flag used to be ignored,
29
+ which silently changed what `fight` did.
30
+
31
+ Numeric flags are read strictly. `--ticks 1e3` means 1000, `--speed 1.5` means 1.5, and
32
+ `--seed 5abc` is an error rather than a silent 5. A flag that wants a whole number says so when
33
+ given a fraction.
34
+
35
+ `missile-calc` applies the engine's own minimums before calculating, and now tells you when it
36
+ did: ask for `--speed 0.5` and it reports the 1.5 the engine will actually use, naming both. It
37
+ also warns when a build's cast is longer than a match, which no clamp catches because there is
38
+ no maximum on damage - the DPS figure for such a build is theoretical, since the missile is
39
+ never launched.
40
+
41
+ ## Pull safety
42
+
43
+ `pull` overwrites freely when the local file is unchanged since the CLI last wrote or uploaded
44
+ it, and refuses otherwise - it keeps a hash of what it left on disk in `.vibemancer-state.json`.
45
+ Commit that file; it belongs with the source, and a teammate benefits from it.
46
+
47
+ It is deliberately quiet in the ordinary case. A flag needed on every pull is a flag everyone
48
+ passes by default, which protects nothing while looking like it does - so `--force` exists for
49
+ the rare case and should not become a habit. A freshly scaffolded project already has its
50
+ starter bot recorded, so the first `npm run pull` is not a refusal.
51
+
52
+ If the record is lost, the cost is one `--force`, never data.
53
+
24
54
  ## Getting started
25
55
 
26
56
  You don't install this directly: `npm create vibemancer@latest my-wizard` sets up a project with the CLI as a dev dependency.
@@ -44,6 +44,7 @@ function clearCredentials() {
44
44
 
45
45
  // src/auth/oauth-client.ts
46
46
  var MCP_BASE_URL = process.env.VIBEMANCER_MCP_URL ?? "https://mcp.vibemancer.com";
47
+ var AUTH_TIMEOUT_MS = 6e4;
47
48
  var NotLoggedInError = class extends Error {
48
49
  constructor() {
49
50
  super("Not logged in. Run: vibemancer login");
@@ -59,7 +60,8 @@ async function registerClient(redirectUri) {
59
60
  const res = await fetch(`${MCP_BASE_URL}/register`, {
60
61
  method: "POST",
61
62
  headers: { "Content-Type": "application/json" },
62
- body: JSON.stringify({ redirect_uris: [redirectUri], client_name: "vibemancer-cli" })
63
+ body: JSON.stringify({ redirect_uris: [redirectUri], client_name: "vibemancer-cli" }),
64
+ signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
63
65
  });
64
66
  if (!res.ok) throw new Error(`Client registration failed (${res.status}).`);
65
67
  const json = await res.json();
@@ -79,7 +81,8 @@ async function exchangeCode(code, codeVerifier) {
79
81
  const res = await fetch(`${MCP_BASE_URL}/token`, {
80
82
  method: "POST",
81
83
  headers: { "Content-Type": "application/json" },
82
- body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: codeVerifier })
84
+ body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: codeVerifier }),
85
+ signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
83
86
  });
84
87
  if (!res.ok) throw new Error(`Token exchange failed (${res.status}): ${await res.text()}`);
85
88
  const creds = parseTokenResponse(await res.json(), "");
@@ -90,7 +93,8 @@ async function refreshCredentials(refreshToken) {
90
93
  const res = await fetch(`${MCP_BASE_URL}/token`, {
91
94
  method: "POST",
92
95
  headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken })
96
+ body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken }),
97
+ signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
94
98
  });
95
99
  if (!res.ok) throw new NotLoggedInError();
96
100
  const creds = parseTokenResponse(await res.json(), refreshToken);
@@ -110,10 +114,11 @@ export {
110
114
  loadCredentials,
111
115
  clearCredentials,
112
116
  MCP_BASE_URL,
117
+ AUTH_TIMEOUT_MS,
113
118
  NotLoggedInError,
114
119
  generatePkce,
115
120
  registerClient,
116
121
  exchangeCode,
117
122
  getAccessToken
118
123
  };
119
- //# sourceMappingURL=chunk-MCU2B4PZ.js.map
124
+ //# sourceMappingURL=chunk-UKMSXLZR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth/oauth-client.ts","../src/auth/credentials-store.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/naming-convention -- OAuth 2.0 wire-format field names (grant_type, access_token, …) are snake_case by spec */\n\n/**\n * CLI OAuth client for the Vibemancer MCP gateway.\n *\n * The CLI is a PKCE OAuth client of mcp.vibemancer.com (the same gateway the MCP\n * AI clients use). `vibemancer login` runs the authorization-code flow; the\n * resulting session token carries the canonical Firebase uid. This module owns\n * PKCE, the token exchange/refresh, and handing callers a fresh access token.\n */\n\nimport {createHash, randomBytes} from 'node:crypto';\nimport {loadCredentials, saveCredentials, type Credentials} from './credentials-store.js';\n\n/** Gateway base URL; overridable for emulator/E2E via VIBEMANCER_MCP_URL. */\nexport const MCP_BASE_URL = process.env.VIBEMANCER_MCP_URL ?? 'https://mcp.vibemancer.com';\n\n/**\n * How long any auth round-trip may take before the CLI gives up.\n *\n * node's fetch has NO default timeout, so a connection that is accepted and then never\n * answered hangs forever — not slow, stuck, with no error and no exit code. These are small\n * JSON calls, so a minute is already many times a cold start of the gateway function; it is\n * set high enough that hitting it means something is genuinely wrong rather than merely slow.\n */\nexport const AUTH_TIMEOUT_MS = 60_000;\n\nexport class NotLoggedInError extends Error\n{\n\tconstructor()\n\t{\n\t\tsuper('Not logged in. Run: vibemancer login');\n\t\tthis.name = 'NotLoggedInError';\n\t}\n}\n\nexport interface Pkce\n{\n\tverifier: string;\n\tchallenge: string;\n}\n\nexport function generatePkce(): Pkce\n{\n\tconst verifier = randomBytes(32).toString('base64url');\n\tconst challenge = createHash('sha256').update(verifier).digest('base64url');\n\treturn {verifier, challenge};\n}\n\n/** Dynamic client registration → client_id (localhost/https redirect allowed). */\nexport async function registerClient(redirectUri: string): Promise<string>\n{\n\tconst res = await fetch(`${MCP_BASE_URL}/register`, {\n\t\tmethod: 'POST',\n\t\theaders: {'Content-Type': 'application/json'},\n\t\tbody: JSON.stringify({redirect_uris: [redirectUri], client_name: 'vibemancer-cli'}),\n\t\tsignal: AbortSignal.timeout(AUTH_TIMEOUT_MS),\n\t});\n\tif (!res.ok) throw new Error(`Client registration failed (${res.status}).`);\n\tconst json: unknown = await res.json();\n\tconst clientId = typeof json === 'object' && json !== null && 'client_id' in json && typeof json.client_id === 'string'\n\t\t? json.client_id\n\t\t: '';\n\tif (!clientId) throw new Error('Client registration returned no client_id.');\n\treturn clientId;\n}\n\nfunction parseTokenResponse(json: unknown, fallbackRefresh: string): Credentials\n{\n\tif (typeof json !== 'object' || json === null) throw new Error('Invalid token response.');\n\tconst accessToken = 'access_token' in json && typeof json.access_token === 'string' ? json.access_token : '';\n\tif (!accessToken) throw new Error('Token response missing access_token.');\n\tconst refreshToken = 'refresh_token' in json && typeof json.refresh_token === 'string' ? json.refresh_token : fallbackRefresh;\n\tconst expiresIn = 'expires_in' in json && typeof json.expires_in === 'number' ? json.expires_in : 3600;\n\treturn {accessToken, refreshToken, expiresAt: Date.now() + expiresIn * 1000};\n}\n\n/** Exchange an authorization code (PKCE) for tokens, and persist them. */\nexport async function exchangeCode(code: string, codeVerifier: string): Promise<Credentials>\n{\n\tconst res = await fetch(`${MCP_BASE_URL}/token`, {\n\t\tmethod: 'POST',\n\t\theaders: {'Content-Type': 'application/json'},\n\t\tbody: JSON.stringify({grant_type: 'authorization_code', code, code_verifier: codeVerifier}),\n\t\tsignal: AbortSignal.timeout(AUTH_TIMEOUT_MS),\n\t});\n\tif (!res.ok) throw new Error(`Token exchange failed (${res.status}): ${await res.text()}`);\n\tconst creds = parseTokenResponse(await res.json(), '');\n\tsaveCredentials(creds);\n\treturn creds;\n}\n\nasync function refreshCredentials(refreshToken: string): Promise<Credentials>\n{\n\tconst res = await fetch(`${MCP_BASE_URL}/token`, {\n\t\tmethod: 'POST',\n\t\theaders: {'Content-Type': 'application/json'},\n\t\tbody: JSON.stringify({grant_type: 'refresh_token', refresh_token: refreshToken}),\n\t\tsignal: AbortSignal.timeout(AUTH_TIMEOUT_MS),\n\t});\n\tif (!res.ok) throw new NotLoggedInError();\n\tconst creds = parseTokenResponse(await res.json(), refreshToken);\n\tsaveCredentials(creds);\n\treturn creds;\n}\n\nconst REFRESH_SKEW_MS = 60_000;\n\n/** A valid access token, refreshing silently when within 60s of expiry. */\nexport async function getAccessToken(): Promise<string>\n{\n\tconst creds = loadCredentials();\n\tif (!creds) throw new NotLoggedInError();\n\tif (creds.expiresAt - Date.now() > REFRESH_SKEW_MS) return creds.accessToken;\n\tconst refreshed = await refreshCredentials(creds.refreshToken);\n\treturn refreshed.accessToken;\n}\n","/**\n * Local credential store for the CLI's OAuth session.\n *\n * Persists the refresh + access tokens at ~/.vibemancer/credentials.json so the\n * user stays logged in between commands. The directory is overridable via\n * VIBEMANCER_CONFIG_DIR (tests, or users who relocate their config).\n */\n\nimport {chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync} from 'node:fs';\nimport {homedir} from 'node:os';\nimport {join} from 'node:path';\n\nexport interface Credentials\n{\n\trefreshToken: string;\n\taccessToken: string;\n\t/** Epoch milliseconds at which the access token expires. */\n\texpiresAt: number;\n}\n\nfunction configDir(): string\n{\n\treturn process.env.VIBEMANCER_CONFIG_DIR ?? join(homedir(), '.vibemancer');\n}\n\nfunction credentialsPath(): string\n{\n\treturn join(configDir(), 'credentials.json');\n}\n\nexport function loadCredentials(): Credentials | null\n{\n\tconst path = credentialsPath();\n\tif (!existsSync(path)) return null;\n\ttry\n\t{\n\t\tconst parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'));\n\t\tif (\n\t\t\ttypeof parsed === 'object' && parsed !== null\n\t\t\t&& 'refreshToken' in parsed && typeof parsed.refreshToken === 'string'\n\t\t\t&& 'accessToken' in parsed && typeof parsed.accessToken === 'string'\n\t\t\t&& 'expiresAt' in parsed && typeof parsed.expiresAt === 'number'\n\t\t)\n\t\t{\n\t\t\treturn {refreshToken: parsed.refreshToken, accessToken: parsed.accessToken, expiresAt: parsed.expiresAt};\n\t\t}\n\t\treturn null;\n\t}\n\tcatch\n\t{\n\t\treturn null;\n\t}\n}\n\nexport function saveCredentials(credentials: Credentials): void\n{\n\tmkdirSync(configDir(), {recursive: true});\n\tconst path = credentialsPath();\n\twriteFileSync(path, JSON.stringify(credentials, null, 2), {mode: 0o600});\n\t// Best-effort tighten perms (no-op semantics on Windows).\n\ttry\n\t{\n\t\tchmodSync(path, 0o600);\n\t}\n\tcatch\n\t{\n\t\t// ignore — Windows / restricted FS\n\t}\n}\n\nexport function clearCredentials(): void\n{\n\ttry\n\t{\n\t\trmSync(credentialsPath(), {force: true});\n\t}\n\tcatch\n\t{\n\t\t// ignore\n\t}\n}\n"],"mappings":"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","names":[]}