@animaapp/cli 0.5.0 → 0.6.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,57 @@ 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
+ function classifyCredentials() {
17
+ let parsed;
17
18
  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;
19
+ if (!fs.existsSync(CREDENTIALS_FILE)) return { kind: "absent" };
20
+ parsed = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
23
21
  } catch {
24
- return null;
22
+ return { kind: "malformed" };
23
+ }
24
+ const figmaToken = typeof parsed.figmaToken === "string" ? parsed.figmaToken : void 0;
25
+ if (typeof parsed.token !== "string") {
26
+ return figmaToken ? { kind: "figma-only", figmaToken } : { kind: "absent" };
27
+ }
28
+ if (parsed.tokenType === "agent") {
29
+ const required2 = ["teamId", "agentId", "expiresAt", "refreshToken", "refreshExpiresAt"];
30
+ if (required2.some((field) => typeof parsed[field] !== "string")) {
31
+ return { kind: "malformed" };
32
+ }
33
+ return { kind: "agent", credential: parsed, figmaToken };
25
34
  }
35
+ return {
36
+ kind: "account",
37
+ credential: {
38
+ tokenType: "account",
39
+ token: parsed.token,
40
+ teamId: typeof parsed.teamId === "string" ? parsed.teamId : void 0
41
+ },
42
+ figmaToken
43
+ };
44
+ }
45
+ function readCredentials() {
46
+ const stored = classifyCredentials();
47
+ if (stored.kind !== "agent" && stored.kind !== "account") return null;
48
+ return { ...stored.credential, ...stored.figmaToken ? { figmaToken: stored.figmaToken } : {} };
26
49
  }
27
- function writeCredentials(credentials) {
28
- const existing = readCredentials() ?? {};
29
- const merged = { ...existing, ...credentials };
50
+ function saveCredential(credential) {
51
+ const figmaToken = readCredentials()?.figmaToken;
52
+ write({ ...credential, ...figmaToken ? { figmaToken } : {} });
53
+ }
54
+ function saveFigmaToken(figmaToken) {
55
+ const existing = readCredentials();
56
+ write({ ...existing ?? {}, figmaToken });
57
+ }
58
+ function write(credentials) {
59
+ const merged = { ...credentials };
30
60
  for (const key of Object.keys(merged)) {
31
61
  if (merged[key] === void 0) delete merged[key];
32
62
  }
33
63
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
34
- fs.writeFileSync(
35
- CREDENTIALS_FILE,
36
- JSON.stringify(merged, null, 2) + "\n",
37
- { mode: 384 }
38
- );
64
+ const tmp = `${CREDENTIALS_FILE}.${process.pid}.tmp`;
65
+ fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + "\n", { mode: 384 });
66
+ fs.renameSync(tmp, CREDENTIALS_FILE);
39
67
  }
40
68
  function deleteCredentials() {
41
69
  try {
@@ -74,6 +102,17 @@ function extractTeamIdFromJWT(token) {
74
102
  }
75
103
  }
76
104
 
105
+ // src/auth/jwt.ts
106
+ function decodeJwtPayload(token) {
107
+ try {
108
+ const parts = token.split(".");
109
+ if (parts.length !== 3) return void 0;
110
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
111
+ } catch {
112
+ return void 0;
113
+ }
114
+ }
115
+
77
116
  // src/api/errors.ts
78
117
  var EXIT_CODES = {
79
118
  SUCCESS: 0,
@@ -116,37 +155,39 @@ function timeoutError() {
116
155
  );
117
156
  }
118
157
 
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
- };
158
+ // src/auth/agentSession.ts
159
+ function required(value, field) {
160
+ if (value === void 0) {
161
+ throw authError(
162
+ `The server's token response is missing \`${field}\`. Check --api-url points at an Anima API that issues agent credentials.`
163
+ );
142
164
  }
143
- throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
165
+ return value;
166
+ }
167
+ function agentSessionFromTokenResponse(body, carriedOver) {
168
+ const token = typeof body.access_token === "string" ? body.access_token : void 0;
169
+ const claims = decodeJwtPayload(required(token, "access_token"));
170
+ return {
171
+ token,
172
+ teamId: required(typeof claims?.team_id === "string" ? claims.team_id : void 0, "team_id"),
173
+ agentId: required(typeof claims?.agent_id === "string" ? claims.agent_id : void 0, "agent_id"),
174
+ expiresAt: required(
175
+ typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0,
176
+ "exp"
177
+ ),
178
+ refreshToken: required(
179
+ typeof body.refresh_token === "string" ? body.refresh_token : carriedOver?.refreshToken,
180
+ "refresh_token"
181
+ ),
182
+ refreshExpiresAt: required(
183
+ typeof body.refresh_expires_in === "number" ? new Date(Date.now() + body.refresh_expires_in * 1e3).toISOString() : carriedOver?.refreshExpiresAt,
184
+ "refresh_expires_in"
185
+ )
186
+ };
187
+ }
188
+ function persistAgentSession(session) {
189
+ saveCredential({ tokenType: "agent", ...session });
144
190
  }
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
191
 
151
192
  // src/output/log.ts
152
193
  import fs2 from "fs";
@@ -180,6 +221,173 @@ function redact(value) {
180
221
  return out;
181
222
  }
182
223
 
224
+ // src/auth/refresh.ts
225
+ var REFRESH_GRANT_TYPE = "refresh_token";
226
+ function readFailure(body) {
227
+ const raw = body.error;
228
+ const nested = typeof raw === "object" && raw !== null ? raw : null;
229
+ const code = typeof raw === "string" ? raw : typeof nested?.code === "string" ? nested.code : typeof nested?.error === "string" ? nested.error : null;
230
+ const described = typeof body.error_description === "string" ? body.error_description : null;
231
+ const message = described ?? (typeof raw === "string" ? raw : raw == null ? null : JSON.stringify(raw));
232
+ return { code, message };
233
+ }
234
+ async function refreshAccessToken(apiUrl, carriedOver, fetchFn = fetch) {
235
+ const base = apiUrl.replace(/\/$/, "");
236
+ const response = await fetchFn(`${base}/oauth/token`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
239
+ body: new URLSearchParams({
240
+ grant_type: REFRESH_GRANT_TYPE,
241
+ refresh_token: carriedOver.refreshToken
242
+ }).toString()
243
+ });
244
+ const body = await response.json().catch(() => ({}));
245
+ const failure = readFailure(body);
246
+ logEvent("refresh_token_response", { status: response.status, error: failure.code });
247
+ if (!response.ok || typeof body.access_token !== "string") {
248
+ if (failure.code === "invalid_grant") {
249
+ clearStorage();
250
+ throw authError(`${failure.message ?? "Your credentials are no longer valid"}
251
+ Run: anima login`);
252
+ }
253
+ throw apiError(
254
+ `Could not refresh credentials (HTTP ${response.status})${failure.message ? `: ${failure.message}` : ""}`
255
+ );
256
+ }
257
+ const session = agentSessionFromTokenResponse(body, carriedOver);
258
+ saveCredential({ tokenType: "agent", ...session });
259
+ return session;
260
+ }
261
+ async function revokeStoredRefreshToken(apiUrl, fetchFn = fetch) {
262
+ const creds = readCredentials();
263
+ if (creds?.tokenType !== "agent") return false;
264
+ try {
265
+ const response = await fetchFn(`${apiUrl.replace(/\/$/, "")}/oauth/revoke`, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
268
+ body: new URLSearchParams({ token: creds.refreshToken, token_type_hint: "refresh_token" }).toString()
269
+ });
270
+ logEvent("revoke_response", { status: response.status });
271
+ return response.ok;
272
+ } catch (err) {
273
+ logEvent("revoke_failed", { error: err instanceof Error ? err.message : String(err) });
274
+ return false;
275
+ }
276
+ }
277
+
278
+ // src/config/store.ts
279
+ import fs3 from "fs";
280
+ import path3 from "path";
281
+ import os2 from "os";
282
+ var CONFIG_DIR2 = path3.join(os2.homedir(), ".config", "anima");
283
+ var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
284
+ var DEFAULT_API_URL = "https://api.agentgrid.io";
285
+ var CONFIG_KEYS = {
286
+ "api-url": "apiUrl"
287
+ };
288
+ function isConfigKey(key) {
289
+ return key in CONFIG_KEYS;
290
+ }
291
+ function configKeys() {
292
+ return Object.keys(CONFIG_KEYS);
293
+ }
294
+ function readConfig() {
295
+ try {
296
+ if (!fs3.existsSync(CONFIG_FILE)) return {};
297
+ const parsed = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf-8"));
298
+ return parsed && typeof parsed === "object" ? parsed : {};
299
+ } catch {
300
+ return {};
301
+ }
302
+ }
303
+ function setConfig(key, value) {
304
+ const merged = { ...readConfig(), [CONFIG_KEYS[key]]: value };
305
+ fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
306
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
307
+ }
308
+ function deleteConfig() {
309
+ try {
310
+ if (!fs3.existsSync(CONFIG_FILE)) return false;
311
+ fs3.unlinkSync(CONFIG_FILE);
312
+ return true;
313
+ } catch {
314
+ return false;
315
+ }
316
+ }
317
+ function unsetConfig(key) {
318
+ const config = readConfig();
319
+ const field = CONFIG_KEYS[key];
320
+ if (!(field in config)) return false;
321
+ delete config[field];
322
+ fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
323
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
324
+ return true;
325
+ }
326
+ function getConfigPath() {
327
+ return CONFIG_FILE;
328
+ }
329
+ function resolveApiUrl(flagValue) {
330
+ return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
331
+ }
332
+
333
+ // src/auth/token.ts
334
+ var RENEW_BEFORE_EXPIRY_MS = 6e4;
335
+ function needsRenewal(expiresAt) {
336
+ if (!expiresAt) return false;
337
+ const expiry = new Date(expiresAt).getTime();
338
+ return Number.isNaN(expiry) || expiry - Date.now() <= RENEW_BEFORE_EXPIRY_MS;
339
+ }
340
+ async function resolveAuth(apiUrlOverride) {
341
+ const stored = classifyCredentials();
342
+ const envToken = process.env.ANIMA_API_TOKEN;
343
+ if (envToken) {
344
+ return {
345
+ token: envToken,
346
+ teamId: process.env.ANIMA_TEAM_ID,
347
+ figmaToken: process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0),
348
+ source: "env"
349
+ };
350
+ }
351
+ const figmaToken = process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0);
352
+ if (stored.kind === "agent") {
353
+ const { credential } = stored;
354
+ if (needsRenewal(credential.expiresAt)) {
355
+ const renewed = await refreshAccessToken(resolveApiUrl(apiUrlOverride), {
356
+ refreshToken: credential.refreshToken,
357
+ refreshExpiresAt: credential.refreshExpiresAt
358
+ });
359
+ return { token: renewed.token, teamId: renewed.teamId, figmaToken, source: "file", tokenType: "agent" };
360
+ }
361
+ return {
362
+ token: credential.token,
363
+ teamId: credential.teamId,
364
+ figmaToken,
365
+ source: "file",
366
+ tokenType: "agent"
367
+ };
368
+ }
369
+ if (stored.kind === "account") {
370
+ return {
371
+ token: stored.credential.token,
372
+ teamId: stored.credential.teamId,
373
+ figmaToken,
374
+ source: "file",
375
+ tokenType: "account"
376
+ };
377
+ }
378
+ if (stored.kind === "malformed") {
379
+ throw authError(
380
+ `Your credentials file is damaged or incomplete (${getCredentialsPath()}). Run: anima login`
381
+ );
382
+ }
383
+ throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
384
+ }
385
+
386
+ // src/api/mcp.ts
387
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
388
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
389
+ import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
390
+
183
391
  // src/version.ts
184
392
  import { createRequire } from "module";
185
393
  var { version } = createRequire(import.meta.url)("../package.json");
@@ -367,10 +575,10 @@ function printKeyValue(key, value) {
367
575
  }
368
576
  function printSessionResult(result) {
369
577
  console.log();
370
- printSuccess("Playground created successfully!");
578
+ printSuccess("Artifact created successfully!");
371
579
  printKeyValue("Session ID", result.sessionId);
372
580
  if (result.playgroundUrl) {
373
- printKeyValue("Your playground", chalk.cyan.underline(result.playgroundUrl));
581
+ printKeyValue("Your artifact", chalk.cyan.underline(result.playgroundUrl));
374
582
  }
375
583
  if (result.tokenUsage) {
376
584
  printKeyValue("Token usage", result.tokenUsage.toLocaleString());
@@ -383,13 +591,6 @@ function printDeployResult(result) {
383
591
  if (result.url) {
384
592
  printKeyValue("Live URL", chalk.cyan.underline(result.url));
385
593
  }
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
594
  console.log();
394
595
  }
395
596
  function printFilesWritten(outputDir, count) {
@@ -533,78 +734,26 @@ function parsePlaygroundUrl(input) {
533
734
  }
534
735
  }
535
736
 
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 {};
559
- }
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;
573
- }
574
- }
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;
589
- }
590
-
591
737
  // src/commands/create.ts
592
738
  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)."
739
+ "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
740
  ).requiredOption(
595
741
  "-t, --type <type>",
596
742
  "empty (empty repo you push to), import (create FROM your code \u2014 use --from), or AI-generated: p2c, l2c, f2c"
597
743
  ).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
744
  "--framework <fw>",
599
745
  "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) => {
746
+ ).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(
747
+ "--artifact-type <kind>",
748
+ "app | markdown \u2014 what the artifact IS. app (default) needs an index.html; markdown needs .md files"
749
+ ).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) => {
601
750
  const mode = detectOutputMode(opts);
602
751
  initLogger(opts.logFile);
603
752
  const progress = new ProgressReporter(mode, opts.verbose);
604
753
  try {
605
754
  const type = opts.type;
606
755
  validateCreateOptions(type, opts);
607
- const auth = resolveAuth();
756
+ const auth = await resolveAuth(opts.apiUrl);
608
757
  const figmaToken = type === "f2c" ? resolveFigmaToken(opts, auth.figmaToken) : void 0;
609
758
  const args = buildCreateArgs(type, opts);
610
759
  const mcpConfig = { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken };
@@ -617,7 +766,7 @@ var createCommand = new Command("create").description(
617
766
  progress.update(`Uploading project zip (${source.reason})...`);
618
767
  const grant = await withMcp(
619
768
  mcpConfig,
620
- (client) => client.callTool("playground-get_zip_upload_url", {})
769
+ (client) => client.callTool("artifact-get_zip_upload_url", {})
621
770
  );
622
771
  if (!grant.success || !grant.uploadUrl || !grant.zipUploadId) {
623
772
  const message = grant.reason || grant.error || "Could not get a zip upload URL";
@@ -635,11 +784,11 @@ ${grant.nextSteps}` : message, grant);
635
784
  }
636
785
  }
637
786
  progress.start(
638
- type === "empty" ? "Creating empty playground..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} playground...`
787
+ type === "empty" ? "Creating empty artifact..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} artifact...`
639
788
  );
640
789
  const result = await withMcp(
641
790
  mcpConfig,
642
- (client) => client.callTool("playground-create", args, {
791
+ (client) => client.callTool("artifact-create", args, {
643
792
  timeoutMs: parseInt(opts.timeout, 10),
644
793
  onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
645
794
  })
@@ -650,20 +799,20 @@ ${grant.nextSteps}` : message, grant);
650
799
  ${result.nextSteps}` : message, result);
651
800
  }
652
801
  const playgroundUrl = result.playgroundUrl || `${PLAYGROUND_BASE}/${result.sessionId}`;
653
- progress.succeed("Playground created!");
802
+ progress.succeed("Artifact created!");
654
803
  if (mode === "pretty") {
655
804
  printSessionResult({ sessionId: result.sessionId, playgroundUrl });
656
805
  if (result.gitRemoteUrl) {
657
806
  console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
658
807
  console.log();
659
- console.log(" Commit and push to update the live playground.");
808
+ console.log(" Commit and push to update the live artifact.");
660
809
  if (result.expiresAt) {
661
810
  console.log(
662
811
  ` The URL embeds a token that expires at ${result.expiresAt} and cannot be renewed;
663
812
  run ${chalk2.cyan(`anima get-git-token ${result.sessionId}`)} and ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
664
813
  );
665
814
  }
666
- console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
815
+ console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
667
816
  console.log();
668
817
  } else if (type === "empty") {
669
818
  console.log(
@@ -751,9 +900,12 @@ function buildCreateArgs(type, opts) {
751
900
  }
752
901
  const framework = opts.framework ?? "html";
753
902
  const language = opts.language ?? (framework === "react" ? "typescript" : void 0);
903
+ const artifactType = opts.artifactType;
754
904
  const args = {
755
905
  type,
756
- framework,
906
+ // framework describes how to build an app; a document has none.
907
+ ...artifactType === "markdown" ? {} : { framework },
908
+ ...artifactType ? { artifactType } : {},
757
909
  styling: opts.styling
758
910
  };
759
911
  if (language) args.language = language;
@@ -791,7 +943,7 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
791
943
  const mode = detectOutputMode(opts);
792
944
  initLogger(opts.logFile);
793
945
  try {
794
- const auth = resolveAuth();
946
+ const auth = await resolveAuth(opts.apiUrl);
795
947
  const result = await withMcp(
796
948
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
797
949
  (client) => client.callTool("workspace-list_artifacts", {})
@@ -861,30 +1013,16 @@ function formatUpdatedAt(iso) {
861
1013
 
862
1014
  // src/commands/publish.ts
863
1015
  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) => {
1016
+ 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
1017
  const mode = detectOutputMode(opts);
866
1018
  initLogger(opts.logFile);
867
1019
  const progress = new ProgressReporter(mode, opts.verbose);
868
1020
  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}...`);
1021
+ const auth = await resolveAuth(opts.apiUrl);
1022
+ progress.start("Publishing...");
885
1023
  const result = await withMcp(
886
1024
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
887
- (client) => client.callTool("playground-publish", args, {
1025
+ (client) => client.callTool("artifact-publish", { sessionId }, {
888
1026
  onProgress: (p) => progress.update(p.message ?? "Publishing...")
889
1027
  })
890
1028
  );
@@ -898,22 +1036,12 @@ var publishCommand = new Command3("publish").description("Deploy playground to l
898
1036
  }
899
1037
  progress.succeed("Published!");
900
1038
  if (mode === "pretty") {
901
- printDeployResult({
902
- url: result.liveUrl,
903
- packageUrl: result.packageUrl,
904
- packageName: result.packageName,
905
- packageVersion: result.packageVersion
906
- });
1039
+ printDeployResult({ url: result.liveUrl });
907
1040
  } else {
908
1041
  outputResult(mode, {
909
1042
  success: true,
910
1043
  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
- } : {}
1044
+ ...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {}
917
1045
  });
918
1046
  }
919
1047
  } catch (err) {
@@ -959,7 +1087,7 @@ function writeFilesToDisk(files, outputDir) {
959
1087
  }
960
1088
 
961
1089
  // 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) => {
1090
+ 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
1091
  const mode = detectOutputMode(opts);
964
1092
  initLogger(opts.logFile);
965
1093
  const progress = new ProgressReporter(mode, opts.verbose);
@@ -976,7 +1104,7 @@ var codegenCommand = new Command4("codegen").description("Convert Figma design t
976
1104
  "No node IDs provided. Use --nodes or include node-id in the Figma URL."
977
1105
  );
978
1106
  }
979
- const auth = resolveAuth();
1107
+ const auth = await resolveAuth(opts.apiUrl);
980
1108
  const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || auth.figmaToken;
981
1109
  if (!figmaToken) {
982
1110
  throw validationError(
@@ -1045,18 +1173,18 @@ function parseGitTokenResponse(payload) {
1045
1173
  try {
1046
1174
  value = JSON.parse(value);
1047
1175
  } catch {
1048
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1176
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1049
1177
  }
1050
1178
  }
1051
1179
  if (value === null || typeof value !== "object") {
1052
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1180
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1053
1181
  }
1054
1182
  const body = value;
1055
1183
  if (body.success === false) {
1056
1184
  throw apiError(`Git access ${body.error ?? "denied"}: ${(body.reason ?? "").trim()}`);
1057
1185
  }
1058
1186
  if (!body.gitRemoteUrl) {
1059
- throw apiError(`Unexpected response from project-get_git_token: ${preview(payload)}`);
1187
+ throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
1060
1188
  }
1061
1189
  return {
1062
1190
  gitRemoteUrl: body.gitRemoteUrl,
@@ -1066,17 +1194,17 @@ function parseGitTokenResponse(payload) {
1066
1194
  }
1067
1195
 
1068
1196
  // 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) => {
1197
+ 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
1198
  const mode = detectOutputMode(opts);
1071
1199
  initLogger(opts.logFile);
1072
1200
  const progress = new ProgressReporter(mode, opts.verbose);
1073
1201
  try {
1074
- const sessionId = parsePlaygroundUrl(playgroundUrl);
1075
- const auth = resolveAuth();
1202
+ const sessionId = parsePlaygroundUrl(artifactUrl);
1203
+ const auth = await resolveAuth(opts.apiUrl);
1076
1204
  progress.start("Requesting git access...");
1077
1205
  const response = await withMcp(
1078
1206
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1079
- (client) => client.callTool("project-get_git_token", { sessionId })
1207
+ (client) => client.callTool("artifact-get_git_token", { sessionId })
1080
1208
  );
1081
1209
  const grant = parseGitTokenResponse(response);
1082
1210
  logEvent("git_token_granted", { sessionId, access: grant.access, expiresAt: grant.expiresAt });
@@ -1086,7 +1214,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
1086
1214
  console.log(` ${chalk4.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
1087
1215
  console.log();
1088
1216
  if (grant.access === "rw") {
1089
- console.log(" Commit and push to update the live playground.");
1217
+ console.log(" Commit and push to update the live artifact.");
1090
1218
  } else {
1091
1219
  console.log(" Read-only: cloning works, pushing will be rejected.");
1092
1220
  }
@@ -1096,7 +1224,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
1096
1224
  re-run this command and ${chalk4.cyan("git remote set-url origin <new url>")} to keep pushing.`
1097
1225
  );
1098
1226
  }
1099
- console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
1227
+ console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
1100
1228
  console.log();
1101
1229
  } else {
1102
1230
  outputResult(mode, {
@@ -1133,15 +1261,6 @@ import open from "open";
1133
1261
  var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1134
1262
  var DEFAULT_CLIENT_NAME = "AgentGrid CLI";
1135
1263
  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
1264
  async function loginWithDeviceFlow(options) {
1146
1265
  const fetchFn = options.fetchFn ?? fetch;
1147
1266
  const sleep = options.sleepFn ?? defaultSleep;
@@ -1186,22 +1305,9 @@ async function loginWithDeviceFlow(options) {
1186
1305
  const body = await pollResponse.json();
1187
1306
  logEvent("device_token_poll", { status: pollResponse.status, error: body.error });
1188
1307
  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;
1308
+ const session = agentSessionFromTokenResponse(body);
1309
+ persistAgentSession(session);
1310
+ return session;
1205
1311
  }
1206
1312
  switch (body.error) {
1207
1313
  case "authorization_pending":
@@ -1248,15 +1354,6 @@ function parseInvite(input) {
1248
1354
  }
1249
1355
  return { code: trimmed.replace(/\.md$/, "") };
1250
1356
  }
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
1357
  async function loginWithInvite(options) {
1261
1358
  const fetchFn = options.fetchFn ?? fetch;
1262
1359
  const { code } = parseInvite(options.invite);
@@ -1277,28 +1374,15 @@ async function loginWithInvite(options) {
1277
1374
  const description = typeof body.error_description === "string" ? body.error_description : `Invite redemption failed (HTTP ${response.status})`;
1278
1375
  throw new CliError("AUTH_INVALID", description, EXIT_CODES.AUTH_ERROR);
1279
1376
  }
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;
1377
+ const session = agentSessionFromTokenResponse(body);
1378
+ persistAgentSession(session);
1379
+ return session;
1296
1380
  }
1297
1381
 
1298
1382
  // src/commands/login.ts
1299
1383
  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(
1300
1384
  "--print-mcp-config",
1301
- "After login, also print an MCP server config (includes the token) to paste into an MCP-capable agent"
1385
+ "After login, also print an MCP server config (URL only \u2014 the client authorizes itself) to paste into an MCP-capable agent"
1302
1386
  ).action(async (opts) => {
1303
1387
  const mode = detectOutputMode(opts);
1304
1388
  initLogger(opts.logFile);
@@ -1310,17 +1394,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1310
1394
  if (inviteOrigin && readConfig().apiUrl !== apiUrl2) {
1311
1395
  setConfig("api-url", apiUrl2);
1312
1396
  }
1313
- const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2, result.token) : void 0;
1397
+ const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2) : void 0;
1314
1398
  if (mode === "pretty") {
1315
1399
  printSuccess("Connected!");
1316
1400
  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);
1401
+ printKeyValue("Agent", result.agentId);
1402
+ printKeyValue("Team ID", result.teamId);
1403
+ printKeyValue("Token expires", result.expiresAt);
1320
1404
  printInfo("This is a scoped agent token \u2014 it acts within what was approved, and the team can revoke it.");
1321
1405
  if (mcpConfig) {
1322
1406
  console.log();
1323
- printInfo("MCP server config (token expires ~7 days \u2014 a fresh invite renews it):");
1407
+ printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
1324
1408
  console.log();
1325
1409
  console.log(JSON.stringify(mcpConfig, null, 2));
1326
1410
  }
@@ -1381,17 +1465,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1381
1465
  }
1382
1466
  }
1383
1467
  });
1384
- const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl, result.token) : void 0;
1468
+ const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl) : void 0;
1385
1469
  if (mode === "pretty") {
1386
1470
  printSuccess("Connected!");
1387
1471
  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);
1472
+ printKeyValue("Agent", result.agentId);
1473
+ printKeyValue("Team ID", result.teamId);
1474
+ printKeyValue("Token expires", result.expiresAt);
1391
1475
  printInfo("This is a scoped agent token \u2014 it acts within what you approved, and your team can revoke it.");
1392
1476
  if (mcpConfig) {
1393
1477
  console.log();
1394
- printInfo("MCP server config (token expires ~7 days \u2014 re-run `anima login` to refresh):");
1478
+ printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
1395
1479
  console.log();
1396
1480
  console.log(JSON.stringify(mcpConfig, null, 2));
1397
1481
  }
@@ -1418,41 +1502,30 @@ var loginCommand = new Command6("login").description("Log in via the device flow
1418
1502
  throw err;
1419
1503
  }
1420
1504
  });
1421
- function buildMcpConfig(apiUrl, token) {
1505
+ function buildMcpConfig(apiUrl) {
1422
1506
  return {
1423
1507
  mcpServers: {
1424
1508
  anima: {
1425
1509
  type: "http",
1426
- url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`,
1427
- headers: { Authorization: `Bearer ${token}` }
1510
+ url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`
1428
1511
  }
1429
1512
  }
1430
1513
  };
1431
1514
  }
1432
1515
 
1433
1516
  // 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) => {
1517
+ 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
1518
  const mode = detectOutputMode(opts);
1436
1519
  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);
1520
+ const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl));
1441
1521
  if (mode === "pretty") {
1442
- printInfo("MCP server config (add to your MCP client; the token is a secret):");
1522
+ printInfo("MCP server config (add to your MCP client):");
1443
1523
  console.log();
1444
1524
  console.log(JSON.stringify(mcpConfig, null, 2));
1445
1525
  console.log();
1446
- if (expiresAt) {
1447
- printInfo(`Token expires ${expiresAt} \u2014 run \`anima login\` then \`anima mcp-config\` to refresh.`);
1448
- }
1526
+ printInfo("Your client will prompt you to authorize on first use.");
1449
1527
  } else {
1450
- outputResult(mode, {
1451
- success: true,
1452
- mcpConfig,
1453
- ...expiresAt ? { expiresAt } : {},
1454
- ...creds?.agentId && auth.source !== "env" ? { agentId: creds.agentId } : {}
1455
- });
1528
+ outputResult(mode, { success: true, mcpConfig });
1456
1529
  }
1457
1530
  } catch (err) {
1458
1531
  if (err instanceof CliError) {
@@ -1477,13 +1550,12 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1477
1550
  if (opts.token || opts.figmaToken) {
1478
1551
  const toSave = {};
1479
1552
  if (opts.token) {
1480
- toSave.token = opts.token;
1481
- toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token) ?? "";
1553
+ toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token);
1554
+ saveCredential({ tokenType: "account", token: opts.token, teamId: toSave.teamId });
1482
1555
  }
1483
1556
  if (opts.figmaToken) {
1484
- toSave.figmaToken = opts.figmaToken;
1557
+ saveFigmaToken(opts.figmaToken);
1485
1558
  }
1486
- writeCredentials(toSave);
1487
1559
  if (mode === "pretty") {
1488
1560
  printSuccess("Credentials saved to " + getCredentialsPath());
1489
1561
  if (opts.token) {
@@ -1505,6 +1577,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1505
1577
  return;
1506
1578
  }
1507
1579
  if (opts.logout) {
1580
+ const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
1508
1581
  const deleted = clearStorage();
1509
1582
  if (mode === "pretty") {
1510
1583
  if (deleted) {
@@ -1513,13 +1586,13 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1513
1586
  printInfo("No stored credentials found");
1514
1587
  }
1515
1588
  } else {
1516
- outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted });
1589
+ outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted, revoked });
1517
1590
  }
1518
1591
  return;
1519
1592
  }
1520
1593
  if (opts.status) {
1521
1594
  try {
1522
- const auth = resolveAuth();
1595
+ const auth = await resolveAuth(opts.apiUrl);
1523
1596
  const creds = readCredentials();
1524
1597
  const sourceName = auth.source === "env" ? "ANIMA_API_TOKEN env var" : getCredentialsPath();
1525
1598
  const hasFigma = !!(process.env.FIGMA_TOKEN || creds?.figmaToken);
@@ -1527,7 +1600,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1527
1600
  printSuccess("Token configured");
1528
1601
  printKeyValue("Source", sourceName);
1529
1602
  printKeyValue("AgentGrid token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
1530
- printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "personal/API");
1603
+ printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "account (personal API token)");
1531
1604
  if (creds?.tokenType === "agent" && creds.expiresAt) printKeyValue("Expires", creds.expiresAt);
1532
1605
  if (auth.teamId) printKeyValue("Team ID", auth.teamId);
1533
1606
  printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
@@ -1535,7 +1608,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1535
1608
  outputResult(mode, {
1536
1609
  success: true,
1537
1610
  source: auth.source,
1538
- tokenType: auth.tokenType ?? "personal",
1611
+ tokenType: auth.tokenType ?? "account",
1539
1612
  tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
1540
1613
  teamId: auth.teamId,
1541
1614
  expiresAt: creds?.tokenType === "agent" ? creds.expiresAt : void 0,
@@ -1586,40 +1659,42 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
1586
1659
 
1587
1660
  // src/commands/logout.ts
1588
1661
  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) => {
1662
+ 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
1663
  const mode = detectOutputMode(opts);
1664
+ const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
1591
1665
  const hadCredentials = clearStorage();
1592
1666
  deleteConfig();
1593
1667
  if (mode === "pretty") {
1594
1668
  if (hadCredentials) {
1595
1669
  printSuccess("Logged out \u2014 all credentials cleared from " + getCredentialsPath());
1670
+ if (revoked) printInfo("Agent credentials revoked server-side");
1596
1671
  } else {
1597
1672
  printInfo("No stored credentials found");
1598
1673
  }
1599
1674
  } else {
1600
- outputResult(mode, { success: true, action: "logged_out", hadCredentials });
1675
+ outputResult(mode, { success: true, action: "logged_out", hadCredentials, revoked });
1601
1676
  }
1602
1677
  });
1603
1678
 
1604
1679
  // src/commands/unpublish.ts
1605
1680
  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) => {
1681
+ 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
1682
  const mode = detectOutputMode(opts);
1608
1683
  initLogger(opts.logFile);
1609
1684
  const progress = new ProgressReporter(mode, opts.verbose);
1610
1685
  try {
1611
- const auth = resolveAuth();
1686
+ const auth = await resolveAuth(opts.apiUrl);
1612
1687
  progress.start("Unpublishing...");
1613
1688
  const result = await withMcp(
1614
1689
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1615
- (client) => client.callTool("playground-unpublish", { sessionId })
1690
+ (client) => client.callTool("artifact-unpublish", { sessionId })
1616
1691
  );
1617
1692
  if (!result.success) {
1618
1693
  throw apiError(result.reason || result.error || "Unpublish failed", result);
1619
1694
  }
1620
1695
  progress.succeed("Unpublished");
1621
1696
  if (mode === "pretty") {
1622
- printSuccess(result.message || `Playground ${sessionId} is no longer publicly reachable.`);
1697
+ printSuccess(result.message || `Artifact ${sessionId} is no longer publicly reachable.`);
1623
1698
  } else {
1624
1699
  outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
1625
1700
  }
@@ -1639,7 +1714,7 @@ var unpublishCommand = new Command10("unpublish").description("Take a published
1639
1714
 
1640
1715
  // src/commands/update.ts
1641
1716
  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) => {
1717
+ 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
1718
  const mode = detectOutputMode(opts);
1644
1719
  initLogger(opts.logFile);
1645
1720
  const progress = new ProgressReporter(mode, opts.verbose);
@@ -1650,11 +1725,11 @@ var updateCommand = new Command11("update").description("Update a playground's m
1650
1725
  if (opts.privacy && !["public", "private"].includes(opts.privacy)) {
1651
1726
  throw validationError(`Invalid privacy "${opts.privacy}". Must be public or private.`);
1652
1727
  }
1653
- const auth = resolveAuth();
1728
+ const auth = await resolveAuth(opts.apiUrl);
1654
1729
  progress.start("Updating...");
1655
1730
  const result = await withMcp(
1656
1731
  { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1657
- (client) => client.callTool("playground-metadata-update", {
1732
+ (client) => client.callTool("artifact-update_metadata", {
1658
1733
  sessionId,
1659
1734
  ...opts.name ? { name: opts.name } : {},
1660
1735
  ...opts.privacy ? { privacy: opts.privacy } : {}
@@ -1665,7 +1740,7 @@ var updateCommand = new Command11("update").description("Update a playground's m
1665
1740
  }
1666
1741
  progress.succeed("Updated");
1667
1742
  if (mode === "pretty") {
1668
- printSuccess(result.message || `Playground ${sessionId} updated.`);
1743
+ printSuccess(result.message || `Artifact ${sessionId} updated.`);
1669
1744
  } else {
1670
1745
  outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
1671
1746
  }
@@ -1750,18 +1825,79 @@ function assertUrl(value) {
1750
1825
  }
1751
1826
  }
1752
1827
 
1828
+ // src/commands/duplicate.ts
1829
+ import { Command as Command13 } from "commander";
1830
+ import chalk7 from "chalk";
1831
+ function buildDuplicateArgs(artifactUrl, name) {
1832
+ const sessionId = parsePlaygroundUrl(artifactUrl);
1833
+ if (!sessionId) {
1834
+ throw validationError("A source artifact URL or session ID is required");
1835
+ }
1836
+ if (name !== void 0 && (name.length === 0 || name.length > 120)) {
1837
+ throw validationError("--name must be between 1 and 120 characters");
1838
+ }
1839
+ return { sessionId, ...name !== void 0 ? { name } : {} };
1840
+ }
1841
+ function createDuplicateCommand() {
1842
+ return new Command13("duplicate").description(
1843
+ "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."
1844
+ ).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) => {
1845
+ const mode = detectOutputMode(opts);
1846
+ initLogger(opts.logFile);
1847
+ const progress = new ProgressReporter(mode, opts.verbose);
1848
+ try {
1849
+ const args = buildDuplicateArgs(artifactUrl, opts.name);
1850
+ const auth = await resolveAuth(opts.apiUrl);
1851
+ progress.start("Duplicating artifact...");
1852
+ const result = await withMcp(
1853
+ { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
1854
+ (client) => client.callTool("artifact-duplicate", args)
1855
+ );
1856
+ if (!result.success) {
1857
+ throw apiError(result.reason || result.error || "Artifact duplication failed", result);
1858
+ }
1859
+ progress.succeed("Artifact duplicated");
1860
+ if (mode === "pretty") {
1861
+ console.log();
1862
+ printSuccess("Artifact duplicated successfully!");
1863
+ printKeyValue("Name", result.name);
1864
+ printKeyValue("Session ID", result.sessionId);
1865
+ printKeyValue("Source session", result.sourceSessionId);
1866
+ printKeyValue("Artifact", chalk7.cyan.underline(result.playgroundUrl));
1867
+ printKeyValue("Preview", chalk7.cyan.underline(result.previewUrl));
1868
+ printKeyValue("Next steps", result.nextSteps);
1869
+ console.log();
1870
+ } else {
1871
+ outputResult(mode, result);
1872
+ }
1873
+ } catch (err) {
1874
+ progress.fail("Failed");
1875
+ if (err instanceof CliError) {
1876
+ if (mode === "pretty") {
1877
+ printError(err.message);
1878
+ } else {
1879
+ outputError(mode, { code: err.code, message: err.message });
1880
+ }
1881
+ process.exit(err.exitCode);
1882
+ }
1883
+ throw err;
1884
+ }
1885
+ });
1886
+ }
1887
+ var duplicateCommand = createDuplicateCommand();
1888
+
1753
1889
  // 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
1890
+ var program = new Command14().name("anima").description(
1891
+ `AgentGrid CLI \u2014 host, edit, and publish your work on AgentGrid. Every artifact is a
1756
1892
  real git repository; each command runs an AgentGrid tool for you.
1757
1893
 
1758
1894
  THE MAIN FLOW (build something and put it on AgentGrid):
1759
1895
  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
1896
+ 2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live app
1761
1897
  (or: create -t empty --framework html \u2014 an empty repo you push to)
1762
1898
  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
1899
+ every push updates the live artifact.
1900
+ 4. Share the playgroundUrl with your human. That IS sharing \u2014 the artifact
1765
1901
  is visible at its URL. Do NOT run publish unless the human explicitly
1766
1902
  asked for a public site; offer it as a follow-up question instead.
1767
1903
 
@@ -1774,19 +1910,20 @@ TIMING (important for AI agents):
1774
1910
  create -t empty|import \u2192 seconds
1775
1911
  create -t p2c|l2c|f2c \u2192 3-7 minutes AI generation (keep the default 600000ms timeout)
1776
1912
  publish/unpublish \u2192 1-3 minutes
1777
- get-git-token / update \u2192 seconds
1913
+ duplicate / get-git-token / update \u2192 seconds
1778
1914
 
1779
1915
  COMMANDS:
1780
1916
  login Connect (device flow, or --invite to redeem an invite link)
1781
1917
  logout Full local reset \u2014 clears credentials and CLI config
1782
1918
  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)
1919
+ create Make an artifact: import your code, empty repo, or AI-generated (p2c/l2c/f2c)
1920
+ duplicate Copy an artifact into a new, independent artifact
1921
+ get-git-token Fresh git access to an existing artifact (or after token expiry)
1922
+ update Rename an artifact or set visibility public/private (metadata only)
1786
1923
  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)
1924
+ unpublish Take a published artifact offline (inverse of publish)
1788
1925
  mcp-config Print an MCP server config from stored credentials (no new login)
1789
- codegen Convert Figma design to local code files (no playground)
1926
+ codegen Convert Figma design to local code files (no artifact)
1790
1927
  config Store CLI preferences (e.g. a default API URL)
1791
1928
  auth Inspect or manage credentials (status / figma token)
1792
1929
 
@@ -1794,6 +1931,7 @@ EXAMPLES (headless flow):
1794
1931
  npx @animaapp/cli login --invite https://api.agentgrid.io/invite/<code>.md
1795
1932
  npx @animaapp/cli create -t import --from ./my-project
1796
1933
  npx @animaapp/cli create -t empty --framework react --name "My app"
1934
+ npx @animaapp/cli duplicate <artifactUrl> --name "My copy"
1797
1935
  npx @animaapp/cli get-git-token <sessionId>
1798
1936
  npx @animaapp/cli update <sessionId> --privacy public
1799
1937
  npx @animaapp/cli publish <sessionId> # only on explicit request
@@ -1806,6 +1944,7 @@ program.addCommand(loginCommand);
1806
1944
  program.addCommand(logoutCommand);
1807
1945
  program.addCommand(listCommand);
1808
1946
  program.addCommand(createCommand);
1947
+ program.addCommand(duplicateCommand);
1809
1948
  program.addCommand(publishCommand);
1810
1949
  program.addCommand(unpublishCommand);
1811
1950
  program.addCommand(updateCommand);