@animaapp/cli 0.5.1 → 0.7.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command13 } from "commander";
4
+ import { Command as Command14 } from "commander";
5
5
 
6
6
  // src/commands/create.ts
7
7
  import { Command } from "commander";
@@ -13,29 +13,61 @@ import path from "path";
13
13
  import os from "os";
14
14
  var CONFIG_DIR = path.join(os.homedir(), ".config", "anima");
15
15
  var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
16
- function readCredentials() {
16
+ var HANDOFF_FILE = path.join(CONFIG_DIR, "handoff.json");
17
+ function classifyCredentials() {
18
+ let parsed;
17
19
  try {
18
- if (!fs.existsSync(CREDENTIALS_FILE)) return null;
19
- const content = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
20
- const parsed = JSON.parse(content);
21
- if (!parsed.token) return null;
22
- return parsed;
20
+ if (!fs.existsSync(CREDENTIALS_FILE)) return { kind: "absent" };
21
+ parsed = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
23
22
  } catch {
24
- return null;
23
+ return { kind: "malformed" };
24
+ }
25
+ const figmaToken = typeof parsed.figmaToken === "string" ? parsed.figmaToken : void 0;
26
+ if (typeof parsed.token !== "string") {
27
+ return figmaToken ? { kind: "figma-only", figmaToken } : { kind: "absent" };
28
+ }
29
+ if (parsed.tokenType === "agent") {
30
+ const required2 = ["teamId", "agentId", "expiresAt", "refreshToken", "refreshExpiresAt"];
31
+ if (required2.some((field) => typeof parsed[field] !== "string")) {
32
+ return { kind: "malformed" };
33
+ }
34
+ return { kind: "agent", credential: parsed, figmaToken };
25
35
  }
36
+ return {
37
+ kind: "account",
38
+ credential: {
39
+ tokenType: "account",
40
+ token: parsed.token,
41
+ teamId: typeof parsed.teamId === "string" ? parsed.teamId : void 0
42
+ },
43
+ figmaToken
44
+ };
45
+ }
46
+ function readCredentials() {
47
+ const stored = classifyCredentials();
48
+ if (stored.kind !== "agent" && stored.kind !== "account") return null;
49
+ return { ...stored.credential, ...stored.figmaToken ? { figmaToken: stored.figmaToken } : {} };
50
+ }
51
+ function saveCredential(credential) {
52
+ const figmaToken = readCredentials()?.figmaToken;
53
+ write({ ...credential, ...figmaToken ? { figmaToken } : {} });
54
+ }
55
+ function saveFigmaToken(figmaToken) {
56
+ const existing = readCredentials();
57
+ write({ ...existing ?? {}, figmaToken });
26
58
  }
27
- function writeCredentials(credentials) {
28
- const existing = readCredentials() ?? {};
29
- const merged = { ...existing, ...credentials };
59
+ function write(credentials) {
60
+ writeSecretJson(CREDENTIALS_FILE, { ...credentials });
61
+ }
62
+ function writeSecretJson(file, data) {
63
+ const merged = { ...data };
30
64
  for (const key of Object.keys(merged)) {
31
65
  if (merged[key] === void 0) delete merged[key];
32
66
  }
33
67
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
34
- fs.writeFileSync(
35
- CREDENTIALS_FILE,
36
- JSON.stringify(merged, null, 2) + "\n",
37
- { mode: 384 }
38
- );
68
+ const tmp = `${file}.${process.pid}.tmp`;
69
+ fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + "\n", { mode: 384 });
70
+ fs.renameSync(tmp, file);
39
71
  }
40
72
  function deleteCredentials() {
41
73
  try {
@@ -48,8 +80,35 @@ function deleteCredentials() {
48
80
  return false;
49
81
  }
50
82
  }
83
+ function savePendingHandoff(pending) {
84
+ writeSecretJson(HANDOFF_FILE, { ...pending });
85
+ }
86
+ function readPendingHandoff() {
87
+ try {
88
+ if (!fs.existsSync(HANDOFF_FILE)) return null;
89
+ const parsed = JSON.parse(fs.readFileSync(HANDOFF_FILE, "utf-8"));
90
+ const required2 = ["handoffToken", "sessionId", "claimUrl", "claimableUntil", "apiUrl"];
91
+ if (required2.some((field) => typeof parsed[field] !== "string")) return null;
92
+ return parsed;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function clearPendingHandoff() {
98
+ try {
99
+ if (!fs.existsSync(HANDOFF_FILE)) return false;
100
+ fs.unlinkSync(HANDOFF_FILE);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+ function getHandoffPath() {
107
+ return HANDOFF_FILE;
108
+ }
51
109
  function clearStorage() {
52
110
  const had = deleteCredentials();
111
+ clearPendingHandoff();
53
112
  try {
54
113
  if (fs.existsSync(CONFIG_DIR) && fs.readdirSync(CONFIG_DIR).length === 0) {
55
114
  fs.rmdirSync(CONFIG_DIR);
@@ -74,6 +133,17 @@ function extractTeamIdFromJWT(token) {
74
133
  }
75
134
  }
76
135
 
136
+ // src/auth/jwt.ts
137
+ function decodeJwtPayload(token) {
138
+ try {
139
+ const parts = token.split(".");
140
+ if (parts.length !== 3) return void 0;
141
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
142
+ } catch {
143
+ return void 0;
144
+ }
145
+ }
146
+
77
147
  // src/api/errors.ts
78
148
  var EXIT_CODES = {
79
149
  SUCCESS: 0,
@@ -116,37 +186,39 @@ function timeoutError() {
116
186
  );
117
187
  }
118
188
 
119
- // src/auth/token.ts
120
- function resolveAuth() {
121
- const creds = readCredentials();
122
- const envToken = process.env.ANIMA_API_TOKEN;
123
- if (envToken) {
124
- return {
125
- token: envToken,
126
- teamId: process.env.ANIMA_TEAM_ID,
127
- figmaToken: process.env.FIGMA_TOKEN || creds?.figmaToken,
128
- source: "env"
129
- };
130
- }
131
- if (creds) {
132
- if (creds.tokenType === "agent" && creds.expiresAt && new Date(creds.expiresAt) <= /* @__PURE__ */ new Date()) {
133
- throw authError("Your agent token has expired. Run: anima login");
134
- }
135
- return {
136
- token: creds.token,
137
- teamId: creds.teamId,
138
- figmaToken: process.env.FIGMA_TOKEN || creds.figmaToken,
139
- source: "file",
140
- tokenType: creds.tokenType
141
- };
189
+ // src/auth/agentSession.ts
190
+ function required(value, field) {
191
+ if (value === void 0) {
192
+ throw authError(
193
+ `The server's token response is missing \`${field}\`. Check --api-url points at an Anima API that issues agent credentials.`
194
+ );
142
195
  }
143
- throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
196
+ return value;
197
+ }
198
+ function agentSessionFromTokenResponse(body, carriedOver) {
199
+ const token = typeof body.access_token === "string" ? body.access_token : void 0;
200
+ const claims = decodeJwtPayload(required(token, "access_token"));
201
+ return {
202
+ token,
203
+ teamId: required(typeof claims?.team_id === "string" ? claims.team_id : void 0, "team_id"),
204
+ agentId: required(typeof claims?.agent_id === "string" ? claims.agent_id : void 0, "agent_id"),
205
+ expiresAt: required(
206
+ typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0,
207
+ "exp"
208
+ ),
209
+ refreshToken: required(
210
+ typeof body.refresh_token === "string" ? body.refresh_token : carriedOver?.refreshToken,
211
+ "refresh_token"
212
+ ),
213
+ refreshExpiresAt: required(
214
+ typeof body.refresh_expires_in === "number" ? new Date(Date.now() + body.refresh_expires_in * 1e3).toISOString() : carriedOver?.refreshExpiresAt,
215
+ "refresh_expires_in"
216
+ )
217
+ };
218
+ }
219
+ function persistAgentSession(session) {
220
+ saveCredential({ tokenType: "agent", ...session });
144
221
  }
145
-
146
- // src/api/mcp.ts
147
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
148
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
149
- import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
150
222
 
151
223
  // src/output/log.ts
152
224
  import fs2 from "fs";
@@ -180,6 +252,173 @@ function redact(value) {
180
252
  return out;
181
253
  }
182
254
 
255
+ // src/auth/refresh.ts
256
+ var REFRESH_GRANT_TYPE = "refresh_token";
257
+ function readFailure(body) {
258
+ const raw = body.error;
259
+ const nested = typeof raw === "object" && raw !== null ? raw : null;
260
+ const code = typeof raw === "string" ? raw : typeof nested?.code === "string" ? nested.code : typeof nested?.error === "string" ? nested.error : null;
261
+ const described = typeof body.error_description === "string" ? body.error_description : null;
262
+ const message = described ?? (typeof raw === "string" ? raw : raw == null ? null : JSON.stringify(raw));
263
+ return { code, message };
264
+ }
265
+ async function refreshAccessToken(apiUrl, carriedOver, fetchFn = fetch) {
266
+ const base = apiUrl.replace(/\/$/, "");
267
+ const response = await fetchFn(`${base}/oauth/token`, {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
270
+ body: new URLSearchParams({
271
+ grant_type: REFRESH_GRANT_TYPE,
272
+ refresh_token: carriedOver.refreshToken
273
+ }).toString()
274
+ });
275
+ const body = await response.json().catch(() => ({}));
276
+ const failure = readFailure(body);
277
+ logEvent("refresh_token_response", { status: response.status, error: failure.code });
278
+ if (!response.ok || typeof body.access_token !== "string") {
279
+ if (failure.code === "invalid_grant") {
280
+ clearStorage();
281
+ throw authError(`${failure.message ?? "Your credentials are no longer valid"}
282
+ Run: anima login`);
283
+ }
284
+ throw apiError(
285
+ `Could not refresh credentials (HTTP ${response.status})${failure.message ? `: ${failure.message}` : ""}`
286
+ );
287
+ }
288
+ const session = agentSessionFromTokenResponse(body, carriedOver);
289
+ saveCredential({ tokenType: "agent", ...session });
290
+ return session;
291
+ }
292
+ async function revokeStoredRefreshToken(apiUrl, fetchFn = fetch) {
293
+ const creds = readCredentials();
294
+ if (creds?.tokenType !== "agent") return false;
295
+ try {
296
+ const response = await fetchFn(`${apiUrl.replace(/\/$/, "")}/oauth/revoke`, {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
299
+ body: new URLSearchParams({ token: creds.refreshToken, token_type_hint: "refresh_token" }).toString()
300
+ });
301
+ logEvent("revoke_response", { status: response.status });
302
+ return response.ok;
303
+ } catch (err) {
304
+ logEvent("revoke_failed", { error: err instanceof Error ? err.message : String(err) });
305
+ return false;
306
+ }
307
+ }
308
+
309
+ // src/config/store.ts
310
+ import fs3 from "fs";
311
+ import path3 from "path";
312
+ import os2 from "os";
313
+ var CONFIG_DIR2 = path3.join(os2.homedir(), ".config", "anima");
314
+ var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
315
+ var DEFAULT_API_URL = "https://api.agentgrid.io";
316
+ var CONFIG_KEYS = {
317
+ "api-url": "apiUrl"
318
+ };
319
+ function isConfigKey(key) {
320
+ return key in CONFIG_KEYS;
321
+ }
322
+ function configKeys() {
323
+ return Object.keys(CONFIG_KEYS);
324
+ }
325
+ function readConfig() {
326
+ try {
327
+ if (!fs3.existsSync(CONFIG_FILE)) return {};
328
+ const parsed = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf-8"));
329
+ return parsed && typeof parsed === "object" ? parsed : {};
330
+ } catch {
331
+ return {};
332
+ }
333
+ }
334
+ function setConfig(key, value) {
335
+ const merged = { ...readConfig(), [CONFIG_KEYS[key]]: value };
336
+ fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
337
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
338
+ }
339
+ function deleteConfig() {
340
+ try {
341
+ if (!fs3.existsSync(CONFIG_FILE)) return false;
342
+ fs3.unlinkSync(CONFIG_FILE);
343
+ return true;
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+ function unsetConfig(key) {
349
+ const config = readConfig();
350
+ const field = CONFIG_KEYS[key];
351
+ if (!(field in config)) return false;
352
+ delete config[field];
353
+ fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
354
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
355
+ return true;
356
+ }
357
+ function getConfigPath() {
358
+ return CONFIG_FILE;
359
+ }
360
+ function resolveApiUrl(flagValue) {
361
+ return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
362
+ }
363
+
364
+ // src/auth/token.ts
365
+ var RENEW_BEFORE_EXPIRY_MS = 6e4;
366
+ function needsRenewal(expiresAt) {
367
+ if (!expiresAt) return false;
368
+ const expiry = new Date(expiresAt).getTime();
369
+ return Number.isNaN(expiry) || expiry - Date.now() <= RENEW_BEFORE_EXPIRY_MS;
370
+ }
371
+ async function resolveAuth(apiUrlOverride) {
372
+ const stored = classifyCredentials();
373
+ const envToken = process.env.ANIMA_API_TOKEN;
374
+ if (envToken) {
375
+ return {
376
+ token: envToken,
377
+ teamId: process.env.ANIMA_TEAM_ID,
378
+ figmaToken: process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0),
379
+ source: "env"
380
+ };
381
+ }
382
+ const figmaToken = process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0);
383
+ if (stored.kind === "agent") {
384
+ const { credential } = stored;
385
+ if (needsRenewal(credential.expiresAt)) {
386
+ const renewed = await refreshAccessToken(resolveApiUrl(apiUrlOverride), {
387
+ refreshToken: credential.refreshToken,
388
+ refreshExpiresAt: credential.refreshExpiresAt
389
+ });
390
+ return { token: renewed.token, teamId: renewed.teamId, figmaToken, source: "file", tokenType: "agent" };
391
+ }
392
+ return {
393
+ token: credential.token,
394
+ teamId: credential.teamId,
395
+ figmaToken,
396
+ source: "file",
397
+ tokenType: "agent"
398
+ };
399
+ }
400
+ if (stored.kind === "account") {
401
+ return {
402
+ token: stored.credential.token,
403
+ teamId: stored.credential.teamId,
404
+ figmaToken,
405
+ source: "file",
406
+ tokenType: "account"
407
+ };
408
+ }
409
+ if (stored.kind === "malformed") {
410
+ throw authError(
411
+ `Your credentials file is damaged or incomplete (${getCredentialsPath()}). Run: anima login`
412
+ );
413
+ }
414
+ throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
415
+ }
416
+
417
+ // src/api/mcp.ts
418
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
419
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
420
+ import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
421
+
183
422
  // src/version.ts
184
423
  import { createRequire } from "module";
185
424
  var { version } = createRequire(import.meta.url)("../package.json");
@@ -367,10 +606,10 @@ function printKeyValue(key, value) {
367
606
  }
368
607
  function printSessionResult(result) {
369
608
  console.log();
370
- printSuccess("Playground created successfully!");
609
+ printSuccess("Artifact created successfully!");
371
610
  printKeyValue("Session ID", result.sessionId);
372
611
  if (result.playgroundUrl) {
373
- printKeyValue("Your playground", chalk.cyan.underline(result.playgroundUrl));
612
+ printKeyValue("Your artifact", chalk.cyan.underline(result.playgroundUrl));
374
613
  }
375
614
  if (result.tokenUsage) {
376
615
  printKeyValue("Token usage", result.tokenUsage.toLocaleString());
@@ -383,13 +622,6 @@ function printDeployResult(result) {
383
622
  if (result.url) {
384
623
  printKeyValue("Live URL", chalk.cyan.underline(result.url));
385
624
  }
386
- if (result.packageName) {
387
- printKeyValue("Package", result.packageName);
388
- printKeyValue("Version", result.packageVersion ?? "");
389
- if (result.packageUrl) {
390
- printKeyValue("URL", chalk.cyan.underline(result.packageUrl));
391
- }
392
- }
393
625
  console.log();
394
626
  }
395
627
  function printFilesWritten(outputDir, count) {
@@ -533,78 +765,81 @@ function parsePlaygroundUrl(input) {
533
765
  }
534
766
  }
535
767
 
536
- // src/config/store.ts
537
- import fs3 from "fs";
538
- import path3 from "path";
539
- import os2 from "os";
540
- var CONFIG_DIR2 = path3.join(os2.homedir(), ".config", "anima");
541
- var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
542
- var DEFAULT_API_URL = "https://api.agentgrid.io";
543
- var CONFIG_KEYS = {
544
- "api-url": "apiUrl"
545
- };
546
- function isConfigKey(key) {
547
- return key in CONFIG_KEYS;
548
- }
549
- function configKeys() {
550
- return Object.keys(CONFIG_KEYS);
551
- }
552
- function readConfig() {
553
- try {
554
- if (!fs3.existsSync(CONFIG_FILE)) return {};
555
- const parsed = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf-8"));
556
- return parsed && typeof parsed === "object" ? parsed : {};
557
- } catch {
558
- return {};
768
+ // src/api/anonymousCreate.ts
769
+ async function createAnonymousArtifact(request, fetchFn = fetch) {
770
+ const url = `${request.apiUrl.replace(/\/$/, "")}/v1/generationSessions`;
771
+ const body = {
772
+ type: "import",
773
+ ...request.files ? { files: request.files } : {},
774
+ ...request.zipUploadId ? { zipUploadId: request.zipUploadId } : {},
775
+ ...request.framework ? { framework: request.framework } : {},
776
+ ...request.artifactType ? { artifactType: request.artifactType } : {},
777
+ ...request.name ? { name: request.name } : {},
778
+ ...request.handoffToken ? { handoffToken: request.handoffToken } : {},
779
+ ...request.clientName ? { clientName: request.clientName } : {}
780
+ };
781
+ logEvent("anonymous_create_request", { url, type: "import", artifactType: request.artifactType });
782
+ const response = await fetchFn(url, {
783
+ method: "POST",
784
+ headers: { "Content-Type": "application/json" },
785
+ body: JSON.stringify(body)
786
+ });
787
+ const payload = await response.json().catch(() => ({}));
788
+ logEvent("anonymous_create_response", { url, status: response.status, error: payload.error });
789
+ if (!response.ok || payload.success !== true) {
790
+ const message = typeof payload.reason === "string" && payload.reason || typeof payload.error === "string" && payload.error || `Anonymous create failed (HTTP ${response.status})`;
791
+ const nextSteps = typeof payload.nextSteps === "string" ? payload.nextSteps : void 0;
792
+ throw apiError(nextSteps ? `${message}
793
+ ${nextSteps}` : message, payload);
559
794
  }
560
- }
561
- function setConfig(key, value) {
562
- const merged = { ...readConfig(), [CONFIG_KEYS[key]]: value };
563
- fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
564
- fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
565
- }
566
- function deleteConfig() {
567
- try {
568
- if (!fs3.existsSync(CONFIG_FILE)) return false;
569
- fs3.unlinkSync(CONFIG_FILE);
570
- return true;
571
- } catch {
572
- return false;
795
+ for (const field of ["sessionId", "handoffToken", "claimUrl", "claimableUntil"]) {
796
+ if (typeof payload[field] !== "string") {
797
+ throw apiError(`The API's create response is missing \`${field}\`.`, payload);
798
+ }
573
799
  }
800
+ return payload;
574
801
  }
575
- function unsetConfig(key) {
576
- const config = readConfig();
577
- const field = CONFIG_KEYS[key];
578
- if (!(field in config)) return false;
579
- delete config[field];
580
- fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
581
- fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
582
- return true;
583
- }
584
- function getConfigPath() {
585
- return CONFIG_FILE;
586
- }
587
- function resolveApiUrl(flagValue) {
588
- return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
802
+ async function getAnonymousZipUploadUrl(apiUrl, fetchFn = fetch) {
803
+ const url = `${apiUrl.replace(/\/$/, "")}/v1/generationSessions/zip-upload-url`;
804
+ const response = await fetchFn(url, { method: "POST" });
805
+ const payload = await response.json().catch(() => ({}));
806
+ if (!response.ok || payload.success !== true || typeof payload.uploadUrl !== "string") {
807
+ const message = typeof payload.reason === "string" && payload.reason || typeof payload.error === "string" && payload.error || `Could not get a zip upload URL (HTTP ${response.status})`;
808
+ throw apiError(message, payload);
809
+ }
810
+ return { zipUploadId: payload.zipUploadId, uploadUrl: payload.uploadUrl };
589
811
  }
590
812
 
591
813
  // src/commands/create.ts
592
814
  var createCommand = new Command("create").description(
593
- "Create a playground from your own code: empty for code you'll push (instant) or import your existing code (instant). Also supports AI generation: p2c/l2c/f2c (3-7 min)."
815
+ "Create an artifact from your own code: empty for code you'll push (instant) or import your existing code (instant). Also supports AI generation: p2c/l2c/f2c (3-7 min)."
594
816
  ).requiredOption(
595
817
  "-t, --type <type>",
596
818
  "empty (empty repo you push to), import (create FROM your code \u2014 use --from), or AI-generated: p2c, l2c, f2c"
597
819
  ).option("-p, --prompt <prompt>", "Text prompt (p2c)").option("-u, --url <url>", "Website URL (l2c)").option("--file-key <key>", "Figma file key or URL (f2c)").option("--nodes <nodes>", "Figma node IDs, comma-separated (f2c)").option("--figma-token <token>", "Figma personal access token (f2c)").option(
598
820
  "--framework <fw>",
599
821
  "html | react \u2014 the only supported frameworks. REQUIRED for empty (declare react if you will push React code); detected from package.json for import"
600
- ).option("--styling <style>", "tailwind | css | plain_css | css_modules | inline_styles", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--guidelines <text>", "Additional constraints (p2c only)").option("--name <name>", "Project name (empty/import only)").option("--from <path>", "Folder or .zip with your project (import only) \u2014 becomes the first commit").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
822
+ ).option("--styling <style>", "tailwind | css | plain_css | css_modules | inline_styles", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--guidelines <text>", "Additional constraints (p2c only)").option(
823
+ "--artifact-type <kind>",
824
+ "app | markdown \u2014 what the artifact IS. app (default) needs an index.html; markdown needs .md files"
825
+ ).option("--name <name>", "Project name (empty/import only)").option("--from <path>", "Folder or .zip with your project (import only) \u2014 becomes the first commit").option(
826
+ "--anonymous",
827
+ "Create without an account (import only). Returns a claim link to send a human, and a handoff token to log in with once they claim it"
828
+ ).option(
829
+ "--handoff <token>",
830
+ "Add this artifact to an existing claim (--anonymous only), so one claim link covers them all. Defaults to the token from your last anonymous create"
831
+ ).option("--client-name <name>", "How this CLI is credited as the creator of an anonymous artifact", "AgentGrid CLI").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
601
832
  const mode = detectOutputMode(opts);
602
833
  initLogger(opts.logFile);
603
834
  const progress = new ProgressReporter(mode, opts.verbose);
604
835
  try {
605
836
  const type = opts.type;
606
837
  validateCreateOptions(type, opts);
607
- const auth = resolveAuth();
838
+ if (opts.anonymous) {
839
+ await runAnonymousCreate(opts, mode, progress);
840
+ return;
841
+ }
842
+ const auth = await resolveAuth(opts.apiUrl);
608
843
  const figmaToken = type === "f2c" ? resolveFigmaToken(opts, auth.figmaToken) : void 0;
609
844
  const args = buildCreateArgs(type, opts);
610
845
  const mcpConfig = { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken };
@@ -617,7 +852,7 @@ var createCommand = new Command("create").description(
617
852
  progress.update(`Uploading project zip (${source.reason})...`);
618
853
  const grant = await withMcp(
619
854
  mcpConfig,
620
- (client) => client.callTool("playground-get_zip_upload_url", {})
855
+ (client) => client.callTool("artifact-get_zip_upload_url", {})
621
856
  );
622
857
  if (!grant.success || !grant.uploadUrl || !grant.zipUploadId) {
623
858
  const message = grant.reason || grant.error || "Could not get a zip upload URL";
@@ -635,11 +870,11 @@ ${grant.nextSteps}` : message, grant);
635
870
  }
636
871
  }
637
872
  progress.start(
638
- type === "empty" ? "Creating empty playground..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} playground...`
873
+ type === "empty" ? "Creating empty artifact..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} artifact...`
639
874
  );
640
875
  const result = await withMcp(
641
876
  mcpConfig,
642
- (client) => client.callTool("playground-create", args, {
877
+ (client) => client.callTool("artifact-create", args, {
643
878
  timeoutMs: parseInt(opts.timeout, 10),
644
879
  onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
645
880
  })
@@ -650,20 +885,20 @@ ${grant.nextSteps}` : message, grant);
650
885
  ${result.nextSteps}` : message, result);
651
886
  }
652
887
  const playgroundUrl = result.playgroundUrl || `${PLAYGROUND_BASE}/${result.sessionId}`;
653
- progress.succeed("Playground created!");
888
+ progress.succeed("Artifact created!");
654
889
  if (mode === "pretty") {
655
890
  printSessionResult({ sessionId: result.sessionId, playgroundUrl });
656
891
  if (result.gitRemoteUrl) {
657
892
  console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
658
893
  console.log();
659
- console.log(" Commit and push to update the live playground.");
894
+ console.log(" Commit and push to update the live artifact.");
660
895
  if (result.expiresAt) {
661
896
  console.log(
662
897
  ` The URL embeds a token that expires at ${result.expiresAt} and cannot be renewed;
663
898
  run ${chalk2.cyan(`anima get-git-token ${result.sessionId}`)} and ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
664
899
  );
665
900
  }
666
- console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
901
+ console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
667
902
  console.log();
668
903
  } else if (type === "empty") {
669
904
  console.log(
@@ -707,6 +942,88 @@ ${result.nextSteps}` : message, result);
707
942
  throw err;
708
943
  }
709
944
  });
945
+ async function runAnonymousCreate(opts, mode, progress) {
946
+ const apiUrl = resolveApiUrl(opts.apiUrl);
947
+ progress.start("Collecting project files...");
948
+ const source = collectProjectSource(opts.from);
949
+ let files;
950
+ let zipUploadId;
951
+ if (source.kind === "files") {
952
+ files = source.files;
953
+ } else {
954
+ progress.update(`Uploading project zip (${source.reason})...`);
955
+ const grant = await getAnonymousZipUploadUrl(apiUrl);
956
+ const putResponse = await fetch(grant.uploadUrl, {
957
+ method: "PUT",
958
+ body: new Uint8Array(source.buffer)
959
+ });
960
+ if (!putResponse.ok) {
961
+ throw apiError(`Zip upload failed: HTTP ${putResponse.status}`, { status: putResponse.status });
962
+ }
963
+ zipUploadId = grant.zipUploadId;
964
+ }
965
+ progress.start("Creating anonymous artifact...");
966
+ const request = {
967
+ apiUrl,
968
+ files,
969
+ zipUploadId,
970
+ ...opts.framework && opts.artifactType !== "markdown" ? { framework: opts.framework } : {},
971
+ ...opts.artifactType ? { artifactType: opts.artifactType } : {},
972
+ ...opts.name ? { name: opts.name } : {},
973
+ clientName: opts.clientName
974
+ };
975
+ const reusable = opts.handoff ? { handoffToken: opts.handoff } : livePendingHandoff();
976
+ const result = reusable ? await createAnonymousArtifact({ ...request, handoffToken: reusable.handoffToken }).catch((err) => {
977
+ if (!isHandoffTokenInvalid(err)) throw err;
978
+ clearPendingHandoff();
979
+ progress.update("Previous claim is no longer open \u2014 starting a new one...");
980
+ return createAnonymousArtifact(request);
981
+ }) : await createAnonymousArtifact(request);
982
+ const claimUrl = result.claimUrl;
983
+ savePendingHandoff({
984
+ handoffToken: result.handoffToken,
985
+ sessionId: result.sessionId,
986
+ claimUrl,
987
+ claimableUntil: result.claimableUntil,
988
+ apiUrl
989
+ });
990
+ progress.succeed("Artifact created!");
991
+ if (mode === "pretty") {
992
+ printSessionResult({ sessionId: result.sessionId, playgroundUrl: result.previewUrl });
993
+ console.log(" Send your human this link to claim it:");
994
+ console.log(` ${chalk2.cyan.underline(claimUrl)}`);
995
+ console.log();
996
+ console.log(` Claimable until ${result.claimableUntil}. Until then the artifact is read-only.`);
997
+ console.log(` Then run ${chalk2.cyan("anima login --handoff")} to pick up your credentials.`);
998
+ if (result.notes) console.log(` Note: ${result.notes}`);
999
+ console.log();
1000
+ } else {
1001
+ outputResult(mode, {
1002
+ success: true,
1003
+ sessionId: result.sessionId,
1004
+ previewUrl: result.previewUrl,
1005
+ claimUrl,
1006
+ claimableUntil: result.claimableUntil,
1007
+ // Returned, not just stored: it is delivered exactly once and can never
1008
+ // be re-fetched, so a caller whose config does not outlive the process
1009
+ // would otherwise lose the only route to a credential.
1010
+ handoffToken: result.handoffToken,
1011
+ handoffStored: getHandoffPath(),
1012
+ ...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
1013
+ ...result.skippedFiles?.length ? { skippedFiles: result.skippedFiles } : {},
1014
+ ...result.notes ? { notes: result.notes } : {},
1015
+ nextSteps: `Send claimUrl to your human \u2014 that is the link that transfers the artifact to them. handoffToken is yours, never theirs: use it to log in (\`anima login --handoff\`, which waits until they claim and then stores your credentials), or to add more artifacts to the same claim (\`anima create --anonymous --handoff <token>\`). Run the login in the background if you have other work.`
1016
+ });
1017
+ }
1018
+ }
1019
+ function livePendingHandoff() {
1020
+ const pending = readPendingHandoff();
1021
+ if (!pending) return null;
1022
+ return new Date(pending.claimableUntil).getTime() > Date.now() ? pending : null;
1023
+ }
1024
+ function isHandoffTokenInvalid(err) {
1025
+ return err instanceof CliError && /handoff_token_invalid/.test(JSON.stringify(err.detail ?? ""));
1026
+ }
710
1027
  function validateCreateOptions(type, opts) {
711
1028
  if (!["empty", "import", "p2c", "l2c", "f2c"].includes(type)) {
712
1029
  throw validationError(`Invalid type "${type}". Must be empty, import, p2c, l2c, or f2c.`);
@@ -720,11 +1037,27 @@ function validateCreateOptions(type, opts) {
720
1037
  if (type === "f2c" && !opts.fileKey) {
721
1038
  throw validationError("--file-key is required for f2c type");
722
1039
  }
723
- if (type === "empty" && !opts.framework) {
1040
+ if (type === "empty" && !opts.framework && opts.artifactType !== "markdown") {
724
1041
  throw validationError(
725
1042
  "--framework is required for type empty \u2014 html for plain HTML/CSS/JS, react for React (declare react if you will push React code)"
726
1043
  );
727
1044
  }
1045
+ if (opts.artifactType && !["app", "markdown"].includes(opts.artifactType)) {
1046
+ throw validationError(
1047
+ `Invalid --artifact-type "${opts.artifactType}". Must be app or markdown.`
1048
+ );
1049
+ }
1050
+ if (opts.handoff && !opts.anonymous) {
1051
+ throw validationError(
1052
+ "--handoff only applies to --anonymous creates \u2014 it adds the new artifact to an existing claim. An authenticated create already belongs to your team."
1053
+ );
1054
+ }
1055
+ if (opts.anonymous && type !== "import") {
1056
+ throw validationError(
1057
+ `--anonymous only works with -t import (got "${type}"). Anonymous artifacts are created FROM code you already have: there is no way to push to an empty one, and AI generation needs an account.
1058
+ Log in first for the other types: anima login`
1059
+ );
1060
+ }
728
1061
  if (type === "import" && !opts.from) {
729
1062
  throw validationError("--from <folder|zip> is required for type import \u2014 the path to the project to import");
730
1063
  }
@@ -745,15 +1078,21 @@ function buildCreateArgs(type, opts) {
745
1078
  if (type === "empty" || type === "import") {
746
1079
  return {
747
1080
  type,
748
- ...opts.framework ? { framework: opts.framework } : {},
1081
+ // A document has no framework to build with, and declaring one on an
1082
+ // import is what made markdown artifacts render as empty apps.
1083
+ ...opts.framework && opts.artifactType !== "markdown" ? { framework: opts.framework } : {},
1084
+ ...opts.artifactType ? { artifactType: opts.artifactType } : {},
749
1085
  ...opts.name ? { name: opts.name } : {}
750
1086
  };
751
1087
  }
752
1088
  const framework = opts.framework ?? "html";
753
1089
  const language = opts.language ?? (framework === "react" ? "typescript" : void 0);
1090
+ const artifactType = opts.artifactType;
754
1091
  const args = {
755
1092
  type,
756
- framework,
1093
+ // framework describes how to build an app; a document has none.
1094
+ ...artifactType === "markdown" ? {} : { framework },
1095
+ ...artifactType ? { artifactType } : {},
757
1096
  styling: opts.styling
758
1097
  };
759
1098
  if (language) args.language = language;
@@ -791,7 +1130,7 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
791
1130
  const mode = detectOutputMode(opts);
792
1131
  initLogger(opts.logFile);
793
1132
  try {
794
- const auth = resolveAuth();
1133
+ const auth = await resolveAuth(opts.apiUrl);
795
1134
  const result = await withMcp(
796
1135
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
797
1136
  (client) => client.callTool("workspace-list_artifacts", {})
@@ -861,30 +1200,16 @@ function formatUpdatedAt(iso) {
861
1200
 
862
1201
  // src/commands/publish.ts
863
1202
  import { Command as Command3 } from "commander";
864
- var publishCommand = new Command3("publish").description("Deploy playground to live URL or npm package (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--mode <mode>", "webapp | designSystem", "webapp").option("--package-name <name>", "NPM package name (designSystem mode)").option("--package-version <version>", "NPM package version (designSystem mode)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
1203
+ var publishCommand = new Command3("publish").description("Deploy an artifact's app to a live public URL (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
865
1204
  const mode = detectOutputMode(opts);
866
1205
  initLogger(opts.logFile);
867
1206
  const progress = new ProgressReporter(mode, opts.verbose);
868
1207
  try {
869
- if (!["webapp", "designSystem"].includes(opts.mode)) {
870
- throw validationError(
871
- `Invalid mode "${opts.mode}". Must be webapp or designSystem.`
872
- );
873
- }
874
- if (opts.mode === "designSystem" && !opts.packageName) {
875
- throw validationError("--package-name is required for designSystem mode");
876
- }
877
- const auth = resolveAuth();
878
- const args = {
879
- sessionId,
880
- mode: opts.mode
881
- };
882
- if (opts.packageName) args.packageName = opts.packageName;
883
- if (opts.packageVersion) args.packageVersion = opts.packageVersion;
884
- progress.start(`Publishing as ${opts.mode}...`);
1208
+ const auth = await resolveAuth(opts.apiUrl);
1209
+ progress.start("Publishing...");
885
1210
  const result = await withMcp(
886
1211
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
887
- (client) => client.callTool("playground-publish", args, {
1212
+ (client) => client.callTool("artifact-publish", { sessionId }, {
888
1213
  onProgress: (p) => progress.update(p.message ?? "Publishing...")
889
1214
  })
890
1215
  );
@@ -898,22 +1223,12 @@ var publishCommand = new Command3("publish").description("Deploy playground to l
898
1223
  }
899
1224
  progress.succeed("Published!");
900
1225
  if (mode === "pretty") {
901
- printDeployResult({
902
- url: result.liveUrl,
903
- packageUrl: result.packageUrl,
904
- packageName: result.packageName,
905
- packageVersion: result.packageVersion
906
- });
1226
+ printDeployResult({ url: result.liveUrl });
907
1227
  } else {
908
1228
  outputResult(mode, {
909
1229
  success: true,
910
1230
  sessionId,
911
- ...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {},
912
- ...result.packageName ? {
913
- packageName: result.packageName,
914
- packageVersion: result.packageVersion,
915
- packageUrl: result.packageUrl
916
- } : {}
1231
+ ...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {}
917
1232
  });
918
1233
  }
919
1234
  } catch (err) {
@@ -959,7 +1274,7 @@ function writeFilesToDisk(files, outputDir) {
959
1274
  }
960
1275
 
961
1276
  // src/commands/codegen.ts
962
- var codegenCommand = new Command4("codegen").description("Convert Figma design to code files (no playground)").requiredOption("--file-key <key>", "Figma file key or URL").option("--nodes <nodes>", "Figma node IDs, comma-separated").option("--figma-token <token>", "Figma personal access token (or set FIGMA_TOKEN env var)").option("-o, --output <dir>", "Output directory", "./generated").option("--framework <fw>", "react | html", "react").option("--styling <style>", "tailwind | plain_css", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
1277
+ var codegenCommand = new Command4("codegen").description("Convert Figma design to code files (no artifact)").requiredOption("--file-key <key>", "Figma file key or URL").option("--nodes <nodes>", "Figma node IDs, comma-separated").option("--figma-token <token>", "Figma personal access token (or set FIGMA_TOKEN env var)").option("-o, --output <dir>", "Output directory", "./generated").option("--framework <fw>", "react | html", "react").option("--styling <style>", "tailwind | plain_css", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
963
1278
  const mode = detectOutputMode(opts);
964
1279
  initLogger(opts.logFile);
965
1280
  const progress = new ProgressReporter(mode, opts.verbose);
@@ -976,7 +1291,7 @@ var codegenCommand = new Command4("codegen").description("Convert Figma design t
976
1291
  "No node IDs provided. Use --nodes or include node-id in the Figma URL."
977
1292
  );
978
1293
  }
979
- const auth = resolveAuth();
1294
+ const auth = await resolveAuth(opts.apiUrl);
980
1295
  const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || auth.figmaToken;
981
1296
  if (!figmaToken) {
982
1297
  throw validationError(
@@ -1045,18 +1360,18 @@ function parseGitTokenResponse(payload) {
1045
1360
  try {
1046
1361
  value = JSON.parse(value);
1047
1362
  } catch {
1048
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1363
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1049
1364
  }
1050
1365
  }
1051
1366
  if (value === null || typeof value !== "object") {
1052
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1367
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1053
1368
  }
1054
1369
  const body = value;
1055
1370
  if (body.success === false) {
1056
1371
  throw apiError(`Git access ${body.error ?? "denied"}: ${(body.reason ?? "").trim()}`);
1057
1372
  }
1058
1373
  if (!body.gitRemoteUrl) {
1059
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1374
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1060
1375
  }
1061
1376
  return {
1062
1377
  gitRemoteUrl: body.gitRemoteUrl,
@@ -1066,17 +1381,17 @@ function parseGitTokenResponse(payload) {
1066
1381
  }
1067
1382
 
1068
1383
  // src/commands/getGitToken.ts
1069
- var getGitTokenCommand = new Command5("get-git-token").description("Mint a short-lived git remote URL for a playground (clone/push with git)").argument("<playgroundUrl>", "Playground URL or session ID").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (playgroundUrl, opts) => {
1384
+ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short-lived git remote URL for an artifact (clone/push with git)").argument("<artifactUrl>", "Artifact URL or session ID").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
1070
1385
  const mode = detectOutputMode(opts);
1071
1386
  initLogger(opts.logFile);
1072
1387
  const progress = new ProgressReporter(mode, opts.verbose);
1073
1388
  try {
1074
- const sessionId = parsePlaygroundUrl(playgroundUrl);
1075
- const auth = resolveAuth();
1389
+ const sessionId = parsePlaygroundUrl(artifactUrl);
1390
+ const auth = await resolveAuth(opts.apiUrl);
1076
1391
  progress.start("Requesting git access...");
1077
1392
  const response = await withMcp(
1078
1393
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1079
- (client) => client.callTool("project-get_git_token", { sessionId })
1394
+ (client) => client.callTool("artifact-get_git_token", { sessionId })
1080
1395
  );
1081
1396
  const grant = parseGitTokenResponse(response);
1082
1397
  logEvent("git_token_granted", { sessionId, access: grant.access, expiresAt: grant.expiresAt });
@@ -1086,7 +1401,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
1086
1401
  console.log(` ${chalk4.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
1087
1402
  console.log();
1088
1403
  if (grant.access === "rw") {
1089
- console.log(" Commit and push to update the live playground.");
1404
+ console.log(" Commit and push to update the live artifact.");
1090
1405
  } else {
1091
1406
  console.log(" Read-only: cloning works, pushing will be rejected.");
1092
1407
  }
@@ -1096,7 +1411,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
1096
1411
  re-run this command and ${chalk4.cyan("git remote set-url origin <new url>")} to keep pushing.`
1097
1412
  );
1098
1413
  }
1099
- console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
1414
+ console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
1100
1415
  console.log();
1101
1416
  } else {
1102
1417
  outputResult(mode, {
@@ -1133,15 +1448,6 @@ import open from "open";
1133
1448
  var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1134
1449
  var DEFAULT_CLIENT_NAME = "AgentGrid CLI";
1135
1450
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1136
- function decodeJwtPayload(token) {
1137
- try {
1138
- const parts = token.split(".");
1139
- if (parts.length !== 3) return void 0;
1140
- return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
1141
- } catch {
1142
- return void 0;
1143
- }
1144
- }
1145
1451
  async function loginWithDeviceFlow(options) {
1146
1452
  const fetchFn = options.fetchFn ?? fetch;
1147
1453
  const sleep = options.sleepFn ?? defaultSleep;
@@ -1186,22 +1492,9 @@ async function loginWithDeviceFlow(options) {
1186
1492
  const body = await pollResponse.json();
1187
1493
  logEvent("device_token_poll", { status: pollResponse.status, error: body.error });
1188
1494
  if (pollResponse.ok && typeof body.access_token === "string") {
1189
- const token = body.access_token;
1190
- const claims = decodeJwtPayload(token);
1191
- const result = {
1192
- token,
1193
- teamId: typeof claims?.team_id === "string" ? claims.team_id : void 0,
1194
- agentId: typeof claims?.agent_id === "string" ? claims.agent_id : void 0,
1195
- expiresAt: typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0
1196
- };
1197
- writeCredentials({
1198
- token: result.token,
1199
- teamId: result.teamId,
1200
- tokenType: "agent",
1201
- agentId: result.agentId,
1202
- expiresAt: result.expiresAt
1203
- });
1204
- return result;
1495
+ const session = agentSessionFromTokenResponse(body);
1496
+ persistAgentSession(session);
1497
+ return session;
1205
1498
  }
1206
1499
  switch (body.error) {
1207
1500
  case "authorization_pending":
@@ -1248,15 +1541,6 @@ function parseInvite(input) {
1248
1541
  }
1249
1542
  return { code: trimmed.replace(/\.md$/, "") };
1250
1543
  }
1251
- function decodeJwtPayload2(token) {
1252
- try {
1253
- const parts = token.split(".");
1254
- if (parts.length !== 3) return void 0;
1255
- return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
1256
- } catch {
1257
- return void 0;
1258
- }
1259
- }
1260
1544
  async function loginWithInvite(options) {
1261
1545
  const fetchFn = options.fetchFn ?? fetch;
1262
1546
  const { code } = parseInvite(options.invite);
@@ -1277,31 +1561,89 @@ async function loginWithInvite(options) {
1277
1561
  const description = typeof body.error_description === "string" ? body.error_description : `Invite redemption failed (HTTP ${response.status})`;
1278
1562
  throw new CliError("AUTH_INVALID", description, EXIT_CODES.AUTH_ERROR);
1279
1563
  }
1280
- const token = body.access_token;
1281
- const claims = decodeJwtPayload2(token);
1282
- const result = {
1283
- token,
1284
- teamId: typeof claims?.team_id === "string" ? claims.team_id : void 0,
1285
- agentId: typeof claims?.agent_id === "string" ? claims.agent_id : void 0,
1286
- expiresAt: typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0
1287
- };
1288
- writeCredentials({
1289
- token: result.token,
1290
- teamId: result.teamId,
1291
- tokenType: "agent",
1292
- agentId: result.agentId,
1293
- expiresAt: result.expiresAt
1294
- });
1295
- return result;
1564
+ const session = agentSessionFromTokenResponse(body);
1565
+ persistAgentSession(session);
1566
+ return session;
1567
+ }
1568
+
1569
+ // src/auth/handoff.ts
1570
+ var HANDOFF_GRANT_TYPE = "urn:animaapp:params:oauth:grant-type:handoff_token";
1571
+ var POLL_INTERVAL_SECONDS = 30;
1572
+ var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1573
+ async function loginWithHandoff(options) {
1574
+ const fetchFn = options.fetchFn ?? fetch;
1575
+ const sleep = options.sleepFn ?? defaultSleep2;
1576
+ const now = options.nowFn ?? Date.now;
1577
+ const base = options.apiUrl.replace(/\/$/, "");
1578
+ const tokenUrl = `${base}/oauth/token`;
1579
+ const deadline = resolveDeadline(options.claimableUntil, now);
1580
+ let intervalMs = POLL_INTERVAL_SECONDS * 1e3;
1581
+ for (; ; ) {
1582
+ const response = await fetchFn(tokenUrl, {
1583
+ method: "POST",
1584
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1585
+ body: new URLSearchParams({
1586
+ grant_type: HANDOFF_GRANT_TYPE,
1587
+ handoff_token: options.handoffToken
1588
+ }).toString()
1589
+ });
1590
+ const body = await response.json().catch(() => ({}));
1591
+ logEvent("handoff_token_poll", { status: response.status, error: body.error });
1592
+ if (response.ok && typeof body.access_token === "string") {
1593
+ const session = agentSessionFromTokenResponse(body);
1594
+ persistAgentSession(session);
1595
+ clearPendingHandoff();
1596
+ return session;
1597
+ }
1598
+ switch (body.error) {
1599
+ case "authorization_pending":
1600
+ break;
1601
+ case "slow_down":
1602
+ intervalMs += 5e3;
1603
+ break;
1604
+ case "expired_token":
1605
+ clearPendingHandoff();
1606
+ throw authError(
1607
+ "This handoff token can no longer be redeemed \u2014 the claim window closed unclaimed, the token was already redeemed, or the human claimed the artifact without granting agent access. Share again with a new `anima create --anonymous` \u2014 or, if they claimed it and want you to keep editing, have them grant your agent from the artifact page."
1608
+ );
1609
+ default:
1610
+ throw new CliError(
1611
+ "API_ERROR",
1612
+ `Unexpected token response: ${body.error ?? `HTTP ${response.status}`}`,
1613
+ EXIT_CODES.API_ERROR
1614
+ );
1615
+ }
1616
+ const remainingMs = deadline.getTime() - now();
1617
+ if (remainingMs <= 0) break;
1618
+ const waitMs = Math.min(intervalMs, remainingMs);
1619
+ options.onPending?.({ nextPollInSeconds: Math.round(waitMs / 1e3), deadline });
1620
+ await sleep(waitMs);
1621
+ }
1622
+ throw authError(
1623
+ "The claim window closed before anyone claimed the artifact. Share again with a new `anima create --anonymous` \u2014 the previous one has expired and can no longer be claimed."
1624
+ );
1625
+ }
1626
+ var FALLBACK_POLL_WINDOW_MS = 24 * 60 * 60 * 1e3;
1627
+ function resolveDeadline(claimableUntil, now) {
1628
+ const parsed = claimableUntil ? new Date(claimableUntil) : null;
1629
+ if (parsed && !Number.isNaN(parsed.getTime())) return parsed;
1630
+ return new Date(now() + FALLBACK_POLL_WINDOW_MS);
1296
1631
  }
1297
1632
 
1298
1633
  // src/commands/login.ts
1299
- var loginCommand = new Command6("login").description("Log in via the device flow, or redeem a pre-approved invite with --invite").option("--invite <url-or-code>", "Redeem an invite link (\u2026/invite/<code>.md) or bare code \u2014 no approval step").option("--client-name <name>", "How this CLI appears on the consent screen", "AgentGrid CLI").option("--no-open", "Do not try to open the verification page in a browser").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").option(
1634
+ var loginCommand = new Command6("login").description("Log in via the device flow, redeem an invite with --invite, or finish an anonymous create with --handoff").option("--invite <url-or-code>", "Redeem an invite link (\u2026/invite/<code>.md) or bare code \u2014 no approval step").option(
1635
+ "--handoff [token]",
1636
+ "Log in with the handoff token from an anonymous create. Waits until a human claims the artifact. Defaults to your last one"
1637
+ ).option("--client-name <name>", "How this CLI appears on the consent screen", "AgentGrid CLI").option("--no-open", "Do not try to open the verification page in a browser").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").option(
1300
1638
  "--print-mcp-config",
1301
- "After login, also print an MCP server config (includes the token) to paste into an MCP-capable agent"
1639
+ "After login, also print an MCP server config (URL only \u2014 the client authorizes itself) to paste into an MCP-capable agent"
1302
1640
  ).action(async (opts) => {
1303
1641
  const mode = detectOutputMode(opts);
1304
1642
  initLogger(opts.logFile);
1643
+ if (opts.handoff) {
1644
+ await runHandoffLogin(opts, mode);
1645
+ return;
1646
+ }
1305
1647
  if (opts.invite) {
1306
1648
  const inviteOrigin = parseInvite(opts.invite).origin;
1307
1649
  const apiUrl2 = opts.apiUrl ? resolveApiUrl(opts.apiUrl) : inviteOrigin ?? resolveApiUrl();
@@ -1310,17 +1652,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1310
1652
  if (inviteOrigin && readConfig().apiUrl !== apiUrl2) {
1311
1653
  setConfig("api-url", apiUrl2);
1312
1654
  }
1313
- const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2, result.token) : void 0;
1655
+ const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2) : void 0;
1314
1656
  if (mode === "pretty") {
1315
1657
  printSuccess("Connected!");
1316
1658
  printKeyValue("Credentials", getCredentialsPath());
1317
- if (result.agentId) printKeyValue("Agent", result.agentId);
1318
- if (result.teamId) printKeyValue("Team ID", result.teamId);
1319
- if (result.expiresAt) printKeyValue("Token expires", result.expiresAt);
1659
+ printKeyValue("Agent", result.agentId);
1660
+ printKeyValue("Team ID", result.teamId);
1661
+ printKeyValue("Token expires", result.expiresAt);
1320
1662
  printInfo("This is a scoped agent token \u2014 it acts within what was approved, and the team can revoke it.");
1321
1663
  if (mcpConfig) {
1322
1664
  console.log();
1323
- printInfo("MCP server config (token expires ~7 days \u2014 a fresh invite renews it):");
1665
+ printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
1324
1666
  console.log();
1325
1667
  console.log(JSON.stringify(mcpConfig, null, 2));
1326
1668
  }
@@ -1381,17 +1723,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1381
1723
  }
1382
1724
  }
1383
1725
  });
1384
- const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl, result.token) : void 0;
1726
+ const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl) : void 0;
1385
1727
  if (mode === "pretty") {
1386
1728
  printSuccess("Connected!");
1387
1729
  printKeyValue("Credentials", getCredentialsPath());
1388
- if (result.agentId) printKeyValue("Agent", result.agentId);
1389
- if (result.teamId) printKeyValue("Team ID", result.teamId);
1390
- if (result.expiresAt) printKeyValue("Token expires", result.expiresAt);
1730
+ printKeyValue("Agent", result.agentId);
1731
+ printKeyValue("Team ID", result.teamId);
1732
+ printKeyValue("Token expires", result.expiresAt);
1391
1733
  printInfo("This is a scoped agent token \u2014 it acts within what you approved, and your team can revoke it.");
1392
1734
  if (mcpConfig) {
1393
1735
  console.log();
1394
- printInfo("MCP server config (token expires ~7 days \u2014 re-run `anima login` to refresh):");
1736
+ printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
1395
1737
  console.log();
1396
1738
  console.log(JSON.stringify(mcpConfig, null, 2));
1397
1739
  }
@@ -1418,41 +1760,113 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1418
1760
  throw err;
1419
1761
  }
1420
1762
  });
1421
- function buildMcpConfig(apiUrl, token) {
1763
+ async function runHandoffLogin(opts, mode) {
1764
+ const explicitToken = typeof opts.handoff === "string" ? opts.handoff : void 0;
1765
+ const stored = readPendingHandoff();
1766
+ const pending = stored && (!explicitToken || stored.handoffToken === explicitToken) ? stored : explicitToken ? { handoffToken: explicitToken, sessionId: "", claimUrl: "", claimableUntil: "", apiUrl: resolveApiUrl() } : null;
1767
+ if (!pending) {
1768
+ const message = "No handoff token. Pass one (`anima login --handoff <token>`), or run `anima create --anonymous -t import --from <path>` to get one. (Already logged in another way? `anima auth --status`.)";
1769
+ if (mode === "pretty") {
1770
+ printError(message);
1771
+ } else {
1772
+ outputError(mode, { code: "VALIDATION_ERROR", message });
1773
+ }
1774
+ process.exit(1);
1775
+ }
1776
+ const apiUrl = opts.apiUrl ? resolveApiUrl(opts.apiUrl) : pending.apiUrl;
1777
+ try {
1778
+ if (mode === "pretty") {
1779
+ console.log();
1780
+ printInfo("Waiting for a human to claim the artifact:");
1781
+ if (pending.claimUrl) {
1782
+ console.log();
1783
+ console.log(` ${chalk5.cyan.underline(pending.claimUrl)}`);
1784
+ }
1785
+ console.log();
1786
+ printInfo(
1787
+ pending.claimableUntil ? `Claimable until ${pending.claimableUntil}. Ctrl-C to stop \u2014 this does not cancel the claim.` : "Ctrl-C to stop \u2014 this does not cancel the claim."
1788
+ );
1789
+ } else {
1790
+ process.stderr.write(
1791
+ JSON.stringify({
1792
+ event: "claim_required",
1793
+ ...pending.claimUrl ? { claimUrl: pending.claimUrl } : {},
1794
+ ...pending.sessionId ? { sessionId: pending.sessionId } : {},
1795
+ ...pending.claimableUntil ? { claimableUntil: pending.claimableUntil } : {}
1796
+ }) + "\n"
1797
+ );
1798
+ }
1799
+ const result = await loginWithHandoff({
1800
+ apiUrl,
1801
+ handoffToken: pending.handoffToken,
1802
+ claimableUntil: pending.claimableUntil || void 0
1803
+ });
1804
+ if (readConfig().apiUrl !== apiUrl) {
1805
+ setConfig("api-url", apiUrl);
1806
+ }
1807
+ const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl) : void 0;
1808
+ if (mode === "pretty") {
1809
+ printSuccess("Claimed \u2014 you're connected!");
1810
+ printKeyValue("Credentials", getCredentialsPath());
1811
+ printKeyValue("Agent", result.agentId);
1812
+ printKeyValue("Team ID", result.teamId);
1813
+ printKeyValue("Token expires", result.expiresAt);
1814
+ printInfo("The artifact is now editable: anima get-git-token " + pending.sessionId);
1815
+ if (mcpConfig) {
1816
+ console.log();
1817
+ printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
1818
+ console.log();
1819
+ console.log(JSON.stringify(mcpConfig, null, 2));
1820
+ }
1821
+ } else {
1822
+ outputResult(mode, {
1823
+ success: true,
1824
+ action: "logged_in",
1825
+ via: "handoff",
1826
+ tokenType: "agent",
1827
+ agentId: result.agentId,
1828
+ teamId: result.teamId,
1829
+ expiresAt: result.expiresAt,
1830
+ sessionId: pending.sessionId,
1831
+ ...mcpConfig ? { mcpConfig } : {}
1832
+ });
1833
+ }
1834
+ } catch (err) {
1835
+ if (err instanceof CliError) {
1836
+ if (mode === "pretty") {
1837
+ printError(err.message);
1838
+ } else {
1839
+ outputError(mode, { code: err.code, message: err.message });
1840
+ }
1841
+ process.exit(err.exitCode);
1842
+ }
1843
+ throw err;
1844
+ }
1845
+ }
1846
+ function buildMcpConfig(apiUrl) {
1422
1847
  return {
1423
1848
  mcpServers: {
1424
1849
  anima: {
1425
1850
  type: "http",
1426
- url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`,
1427
- headers: { Authorization: `Bearer ${token}` }
1851
+ url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`
1428
1852
  }
1429
1853
  }
1430
1854
  };
1431
1855
  }
1432
1856
 
1433
1857
  // src/commands/mcpConfig.ts
1434
- var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP server config from your stored credentials (run `login` first)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
1858
+ var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP server config for your MCP client").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
1435
1859
  const mode = detectOutputMode(opts);
1436
1860
  try {
1437
- const auth = resolveAuth();
1438
- const creds = readCredentials();
1439
- const expiresAt = auth.source === "env" ? void 0 : creds?.expiresAt;
1440
- const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl), auth.token);
1861
+ const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl));
1441
1862
  if (mode === "pretty") {
1442
- printInfo("MCP server config (add to your MCP client; the token is a secret):");
1863
+ printInfo("MCP server config (add to your MCP client):");
1443
1864
  console.log();
1444
1865
  console.log(JSON.stringify(mcpConfig, null, 2));
1445
1866
  console.log();
1446
- if (expiresAt) {
1447
- printInfo(`Token expires ${expiresAt} \u2014 run \`anima login\` then \`anima mcp-config\` to refresh.`);
1448
- }
1867
+ printInfo("Your client will prompt you to authorize on first use.");
1449
1868
  } else {
1450
- outputResult(mode, {
1451
- success: true,
1452
- mcpConfig,
1453
- ...expiresAt ? { expiresAt } : {},
1454
- ...creds?.agentId && auth.source !== "env" ? { agentId: creds.agentId } : {}
1455
- });
1869
+ outputResult(mode, { success: true, mcpConfig });
1456
1870
  }
1457
1871
  } catch (err) {
1458
1872
  if (err instanceof CliError) {
@@ -1471,19 +1885,37 @@ var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP serv
1471
1885
  import { Command as Command8 } from "commander";
1472
1886
  import chalk6 from "chalk";
1473
1887
  var DEFAULT_API_URL2 = "https://api.agentgrid.io";
1888
+ function describePendingHandoff(pending, state) {
1889
+ return {
1890
+ state,
1891
+ sessionId: pending.sessionId,
1892
+ claimUrl: pending.claimUrl,
1893
+ claimableUntil: pending.claimableUntil,
1894
+ nextSteps: state === "expired" ? "The claim window closed. Create another artifact, or ask your human for an agent invite: anima login --invite <url-or-code>" : "Send claimUrl to your human, then run `anima login --handoff` to wait for the credentials their claim grants."
1895
+ };
1896
+ }
1897
+ function printPendingHandoff(pending, state) {
1898
+ console.log();
1899
+ if (state === "expired") {
1900
+ printInfo(`Anonymous artifact ${pending.sessionId} was never claimed \u2014 its window closed at ${pending.claimableUntil}.`);
1901
+ return;
1902
+ }
1903
+ printInfo(`Anonymous artifact ${pending.sessionId} is awaiting a claim (until ${pending.claimableUntil}):`);
1904
+ console.log(` ${chalk6.cyan.underline(pending.claimUrl)}`);
1905
+ console.log(` Run ${chalk6.cyan("anima login --handoff")} to wait for it.`);
1906
+ }
1474
1907
  var authCommand = new Command8("auth").description("Manage authentication").option("--token <token>", "Store AgentGrid API token").option("--figma-token <token>", "Store Figma personal access token").option("--team-id <teamId>", "Team ID to associate with token").option("--status", "Show current auth status").option("--logout", "Clear stored credentials (alias for `anima logout`)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL", DEFAULT_API_URL2).action(async (opts) => {
1475
1908
  const mode = detectOutputMode(opts);
1476
1909
  try {
1477
1910
  if (opts.token || opts.figmaToken) {
1478
1911
  const toSave = {};
1479
1912
  if (opts.token) {
1480
- toSave.token = opts.token;
1481
- toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token) ?? "";
1913
+ toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token);
1914
+ saveCredential({ tokenType: "account", token: opts.token, teamId: toSave.teamId });
1482
1915
  }
1483
1916
  if (opts.figmaToken) {
1484
- toSave.figmaToken = opts.figmaToken;
1917
+ saveFigmaToken(opts.figmaToken);
1485
1918
  }
1486
- writeCredentials(toSave);
1487
1919
  if (mode === "pretty") {
1488
1920
  printSuccess("Credentials saved to " + getCredentialsPath());
1489
1921
  if (opts.token) {
@@ -1505,6 +1937,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1505
1937
  return;
1506
1938
  }
1507
1939
  if (opts.logout) {
1940
+ const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
1508
1941
  const deleted = clearStorage();
1509
1942
  if (mode === "pretty") {
1510
1943
  if (deleted) {
@@ -1513,13 +1946,15 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1513
1946
  printInfo("No stored credentials found");
1514
1947
  }
1515
1948
  } else {
1516
- outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted });
1949
+ outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted, revoked });
1517
1950
  }
1518
1951
  return;
1519
1952
  }
1520
1953
  if (opts.status) {
1954
+ const pending = readPendingHandoff();
1955
+ const pendingState = !pending ? void 0 : new Date(pending.claimableUntil).getTime() <= Date.now() ? "expired" : "awaiting_claim";
1521
1956
  try {
1522
- const auth = resolveAuth();
1957
+ const auth = await resolveAuth(opts.apiUrl);
1523
1958
  const creds = readCredentials();
1524
1959
  const sourceName = auth.source === "env" ? "ANIMA_API_TOKEN env var" : getCredentialsPath();
1525
1960
  const hasFigma = !!(process.env.FIGMA_TOKEN || creds?.figmaToken);
@@ -1527,24 +1962,35 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1527
1962
  printSuccess("Token configured");
1528
1963
  printKeyValue("Source", sourceName);
1529
1964
  printKeyValue("AgentGrid token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
1530
- printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "personal/API");
1965
+ printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "account (personal API token)");
1531
1966
  if (creds?.tokenType === "agent" && creds.expiresAt) printKeyValue("Expires", creds.expiresAt);
1532
1967
  if (auth.teamId) printKeyValue("Team ID", auth.teamId);
1533
1968
  printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
1969
+ if (pending) printPendingHandoff(pending, pendingState);
1534
1970
  } else {
1535
1971
  outputResult(mode, {
1536
1972
  success: true,
1537
1973
  source: auth.source,
1538
- tokenType: auth.tokenType ?? "personal",
1974
+ tokenType: auth.tokenType ?? "account",
1539
1975
  tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
1540
1976
  teamId: auth.teamId,
1541
1977
  expiresAt: creds?.tokenType === "agent" ? creds.expiresAt : void 0,
1542
- figmaToken: hasFigma
1978
+ figmaToken: hasFigma,
1979
+ ...pending ? { pendingHandoff: describePendingHandoff(pending, pendingState) } : {}
1543
1980
  });
1544
1981
  }
1545
1982
  } catch (err) {
1546
1983
  if (err instanceof CliError) {
1547
- if (mode === "pretty") {
1984
+ if (pending && mode === "pretty") {
1985
+ printInfo("Not connected yet.");
1986
+ printPendingHandoff(pending, pendingState);
1987
+ } else if (pending) {
1988
+ outputResult(mode, {
1989
+ success: true,
1990
+ tokenType: null,
1991
+ pendingHandoff: describePendingHandoff(pending, pendingState)
1992
+ });
1993
+ } else if (mode === "pretty") {
1548
1994
  printError(err.message);
1549
1995
  } else {
1550
1996
  outputError(mode, { code: err.code, message: err.message });
@@ -1559,6 +2005,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1559
2005
  console.log();
1560
2006
  printInfo("Auth commands:");
1561
2007
  console.log(` ${chalk6.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
2008
+ console.log(` ${chalk6.cyan("anima login --handoff")} Wait for a human to claim your anonymous artifact`);
1562
2009
  console.log(` ${chalk6.cyan("anima logout")} Clear all credentials`);
1563
2010
  console.log(` ${chalk6.cyan("anima auth --status")} Check auth status`);
1564
2011
  console.log(` ${chalk6.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
@@ -1586,40 +2033,42 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1586
2033
 
1587
2034
  // src/commands/logout.ts
1588
2035
  import { Command as Command9 } from "commander";
1589
- var logoutCommand = new Command9("logout").description("Clear all stored credentials (AgentGrid + Figma tokens) from this machine").option("--json", "Output as JSON").action((opts) => {
2036
+ var logoutCommand = new Command9("logout").description("Clear all stored credentials (AgentGrid + Figma tokens) from this machine").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
1590
2037
  const mode = detectOutputMode(opts);
2038
+ const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
1591
2039
  const hadCredentials = clearStorage();
1592
2040
  deleteConfig();
1593
2041
  if (mode === "pretty") {
1594
2042
  if (hadCredentials) {
1595
2043
  printSuccess("Logged out \u2014 all credentials cleared from " + getCredentialsPath());
2044
+ if (revoked) printInfo("Agent credentials revoked server-side");
1596
2045
  } else {
1597
2046
  printInfo("No stored credentials found");
1598
2047
  }
1599
2048
  } else {
1600
- outputResult(mode, { success: true, action: "logged_out", hadCredentials });
2049
+ outputResult(mode, { success: true, action: "logged_out", hadCredentials, revoked });
1601
2050
  }
1602
2051
  });
1603
2052
 
1604
2053
  // src/commands/unpublish.ts
1605
2054
  import { Command as Command10 } from "commander";
1606
- var unpublishCommand = new Command10("unpublish").description("Take a published playground offline (clears its live URL; code and playground stay intact)").argument("<sessionId>", "Session ID of the published playground").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
2055
+ var unpublishCommand = new Command10("unpublish").description("Take a published artifact offline (clears its live URL; code and artifact stay intact)").argument("<sessionId>", "Session ID of the published artifact").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
1607
2056
  const mode = detectOutputMode(opts);
1608
2057
  initLogger(opts.logFile);
1609
2058
  const progress = new ProgressReporter(mode, opts.verbose);
1610
2059
  try {
1611
- const auth = resolveAuth();
2060
+ const auth = await resolveAuth(opts.apiUrl);
1612
2061
  progress.start("Unpublishing...");
1613
2062
  const result = await withMcp(
1614
2063
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1615
- (client) => client.callTool("playground-unpublish", { sessionId })
2064
+ (client) => client.callTool("artifact-unpublish", { sessionId })
1616
2065
  );
1617
2066
  if (!result.success) {
1618
2067
  throw apiError(result.reason || result.error || "Unpublish failed", result);
1619
2068
  }
1620
2069
  progress.succeed("Unpublished");
1621
2070
  if (mode === "pretty") {
1622
- printSuccess(result.message || `Playground ${sessionId} is no longer publicly reachable.`);
2071
+ printSuccess(result.message || `Artifact ${sessionId} is no longer publicly reachable.`);
1623
2072
  } else {
1624
2073
  outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
1625
2074
  }
@@ -1639,7 +2088,7 @@ var unpublishCommand = new Command10("unpublish").description("Take a published
1639
2088
 
1640
2089
  // src/commands/update.ts
1641
2090
  import { Command as Command11 } from "commander";
1642
- var updateCommand = new Command11("update").description("Update a playground's metadata: display name and/or visibility (never touches code)").argument("<sessionId>", "Session ID of the playground").option("--name <name>", "New display name").option("--privacy <privacy>", "public (anyone with the link) | private (team only)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
2091
+ var updateCommand = new Command11("update").description("Update an artifact's metadata: display name and/or visibility (never touches code)").argument("<sessionId>", "Session ID of the artifact").option("--name <name>", "New display name").option("--privacy <privacy>", "public (anyone with the link) | private (team only)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
1643
2092
  const mode = detectOutputMode(opts);
1644
2093
  initLogger(opts.logFile);
1645
2094
  const progress = new ProgressReporter(mode, opts.verbose);
@@ -1650,11 +2099,11 @@ var updateCommand = new Command11("update").description("Update a playground's m
1650
2099
  if (opts.privacy && !["public", "private"].includes(opts.privacy)) {
1651
2100
  throw validationError(`Invalid privacy "${opts.privacy}". Must be public or private.`);
1652
2101
  }
1653
- const auth = resolveAuth();
2102
+ const auth = await resolveAuth(opts.apiUrl);
1654
2103
  progress.start("Updating...");
1655
2104
  const result = await withMcp(
1656
2105
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1657
- (client) => client.callTool("playground-metadata-update", {
2106
+ (client) => client.callTool("artifact-update_metadata", {
1658
2107
  sessionId,
1659
2108
  ...opts.name ? { name: opts.name } : {},
1660
2109
  ...opts.privacy ? { privacy: opts.privacy } : {}
@@ -1665,7 +2114,7 @@ var updateCommand = new Command11("update").description("Update a playground's m
1665
2114
  }
1666
2115
  progress.succeed("Updated");
1667
2116
  if (mode === "pretty") {
1668
- printSuccess(result.message || `Playground ${sessionId} updated.`);
2117
+ printSuccess(result.message || `Artifact ${sessionId} updated.`);
1669
2118
  } else {
1670
2119
  outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
1671
2120
  }
@@ -1750,18 +2199,79 @@ function assertUrl(value) {
1750
2199
  }
1751
2200
  }
1752
2201
 
2202
+ // src/commands/duplicate.ts
2203
+ import { Command as Command13 } from "commander";
2204
+ import chalk7 from "chalk";
2205
+ function buildDuplicateArgs(artifactUrl, name) {
2206
+ const sessionId = parsePlaygroundUrl(artifactUrl);
2207
+ if (!sessionId) {
2208
+ throw validationError("A source artifact URL or session ID is required");
2209
+ }
2210
+ if (name !== void 0 && (name.length === 0 || name.length > 120)) {
2211
+ throw validationError("--name must be between 1 and 120 characters");
2212
+ }
2213
+ return { sessionId, ...name !== void 0 ? { name } : {} };
2214
+ }
2215
+ function createDuplicateCommand() {
2216
+ return new Command13("duplicate").description(
2217
+ "Duplicate an artifact into a new, independent artifact. Copies code, assets, and supported database content; chat and custom domains are not copied. Duplication is non-idempotent: if a request times out or its response is lost, list recent artifacts before retrying because the first request may already have created the copy."
2218
+ ).argument("<artifactUrl>", "Source artifact URL or session ID").option("--name <name>", "Display name for the duplicate (defaults to '<source name> (Copy)')").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
2219
+ const mode = detectOutputMode(opts);
2220
+ initLogger(opts.logFile);
2221
+ const progress = new ProgressReporter(mode, opts.verbose);
2222
+ try {
2223
+ const args = buildDuplicateArgs(artifactUrl, opts.name);
2224
+ const auth = await resolveAuth(opts.apiUrl);
2225
+ progress.start("Duplicating artifact...");
2226
+ const result = await withMcp(
2227
+ { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
2228
+ (client) => client.callTool("artifact-duplicate", args)
2229
+ );
2230
+ if (!result.success) {
2231
+ throw apiError(result.reason || result.error || "Artifact duplication failed", result);
2232
+ }
2233
+ progress.succeed("Artifact duplicated");
2234
+ if (mode === "pretty") {
2235
+ console.log();
2236
+ printSuccess("Artifact duplicated successfully!");
2237
+ printKeyValue("Name", result.name);
2238
+ printKeyValue("Session ID", result.sessionId);
2239
+ printKeyValue("Source session", result.sourceSessionId);
2240
+ printKeyValue("Artifact", chalk7.cyan.underline(result.playgroundUrl));
2241
+ printKeyValue("Preview", chalk7.cyan.underline(result.previewUrl));
2242
+ printKeyValue("Next steps", result.nextSteps);
2243
+ console.log();
2244
+ } else {
2245
+ outputResult(mode, result);
2246
+ }
2247
+ } catch (err) {
2248
+ progress.fail("Failed");
2249
+ if (err instanceof CliError) {
2250
+ if (mode === "pretty") {
2251
+ printError(err.message);
2252
+ } else {
2253
+ outputError(mode, { code: err.code, message: err.message });
2254
+ }
2255
+ process.exit(err.exitCode);
2256
+ }
2257
+ throw err;
2258
+ }
2259
+ });
2260
+ }
2261
+ var duplicateCommand = createDuplicateCommand();
2262
+
1753
2263
  // src/index.ts
1754
- var program = new Command13().name("anima").description(
1755
- `AgentGrid CLI \u2014 host, edit, and publish your work on AgentGrid. Every playground is a
2264
+ var program = new Command14().name("anima").description(
2265
+ `AgentGrid CLI \u2014 host, edit, and publish your work on AgentGrid. Every artifact is a
1756
2266
  real git repository; each command runs an AgentGrid tool for you.
1757
2267
 
1758
2268
  THE MAIN FLOW (build something and put it on AgentGrid):
1759
2269
  1. npx @animaapp/cli login # once; a human approves
1760
- 2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live playground
2270
+ 2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live app
1761
2271
  (or: create -t empty --framework html \u2014 an empty repo you push to)
1762
2272
  3. The response includes a rw gitRemoteUrl \u2014 git clone, edit, commit, push:
1763
- every push updates the live playground.
1764
- 4. Share the playgroundUrl with your human. That IS sharing \u2014 the playground
2273
+ every push updates the live artifact.
2274
+ 4. Share the playgroundUrl with your human. That IS sharing \u2014 the artifact
1765
2275
  is visible at its URL. Do NOT run publish unless the human explicitly
1766
2276
  asked for a public site; offer it as a follow-up question instead.
1767
2277
 
@@ -1774,19 +2284,20 @@ TIMING (important for AI agents):
1774
2284
  create -t empty|import \u2192 seconds
1775
2285
  create -t p2c|l2c|f2c \u2192 3-7 minutes AI generation (keep the default 600000ms timeout)
1776
2286
  publish/unpublish \u2192 1-3 minutes
1777
- get-git-token / update \u2192 seconds
2287
+ duplicate / get-git-token / update \u2192 seconds
1778
2288
 
1779
2289
  COMMANDS:
1780
2290
  login Connect (device flow, or --invite to redeem an invite link)
1781
2291
  logout Full local reset \u2014 clears credentials and CLI config
1782
2292
  list List your team's artifacts (most-recently-updated first) to resume work
1783
- create Make a playground: import your code, empty repo, or AI-generated (p2c/l2c/f2c)
1784
- get-git-token Fresh git access to an existing playground (or after token expiry)
1785
- update Rename a playground or set visibility public/private (metadata only)
2293
+ create Make an artifact: import your code, empty repo, or AI-generated (p2c/l2c/f2c)
2294
+ duplicate Copy an artifact into a new, independent artifact
2295
+ get-git-token Fresh git access to an existing artifact (or after token expiry)
2296
+ update Rename an artifact or set visibility public/private (metadata only)
1786
2297
  publish Deploy to a public live URL \u2014 ONLY when the human asked for it
1787
- unpublish Take a published playground offline (inverse of publish)
2298
+ unpublish Take a published artifact offline (inverse of publish)
1788
2299
  mcp-config Print an MCP server config from stored credentials (no new login)
1789
- codegen Convert Figma design to local code files (no playground)
2300
+ codegen Convert Figma design to local code files (no artifact)
1790
2301
  config Store CLI preferences (e.g. a default API URL)
1791
2302
  auth Inspect or manage credentials (status / figma token)
1792
2303
 
@@ -1794,6 +2305,7 @@ EXAMPLES (headless flow):
1794
2305
  npx @animaapp/cli login --invite https://api.agentgrid.io/invite/<code>.md
1795
2306
  npx @animaapp/cli create -t import --from ./my-project
1796
2307
  npx @animaapp/cli create -t empty --framework react --name "My app"
2308
+ npx @animaapp/cli duplicate <artifactUrl> --name "My copy"
1797
2309
  npx @animaapp/cli get-git-token <sessionId>
1798
2310
  npx @animaapp/cli update <sessionId> --privacy public
1799
2311
  npx @animaapp/cli publish <sessionId> # only on explicit request
@@ -1806,6 +2318,7 @@ program.addCommand(loginCommand);
1806
2318
  program.addCommand(logoutCommand);
1807
2319
  program.addCommand(listCommand);
1808
2320
  program.addCommand(createCommand);
2321
+ program.addCommand(duplicateCommand);
1809
2322
  program.addCommand(publishCommand);
1810
2323
  program.addCommand(unpublishCommand);
1811
2324
  program.addCommand(updateCommand);