standout 0.5.32 → 0.5.34

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.
Files changed (2) hide show
  1. package/dist/cli.js +97 -56
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
3
  import chalk from "chalk";
4
- import http from "node:http";
4
+ import { writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
5
7
  import { createInterface } from "readline";
6
8
  import { markWrappedShared, STANDOUT_API_URL } from "./api.js";
7
9
  import { capPayload } from "./payload.js";
@@ -59,10 +61,10 @@ const MODE_OPTIONS = [
59
61
  {
60
62
  key: "local",
61
63
  title: "Local only",
62
- tag: " — private & offline",
64
+ tag: " — private",
63
65
  lines: [
64
- "Stays entirely on your computer. Nothing uploaded, nothing saved.",
65
- "Just your local AI-tool stats no LLM cards, score, or ranking.",
66
+ "Your stats stay on your computer no account, no leaderboard, nothing saved.",
67
+ "Renders your wrapped right in the terminal (no LLM cards, score, or ranking).",
66
68
  ],
67
69
  },
68
70
  ];
@@ -249,12 +251,73 @@ async function renderLocalTerminal(prefetched) {
249
251
  });
250
252
  await renderWrappedAll(view);
251
253
  }
252
- // Local path: gather on-device, then open the real wrapped UI on localhost. The
253
- // gathered profile is held only in this process and served from an ephemeral
254
- // in-memory HTTP server — nothing is written to disk, no DB row, no leaderboard
255
- // entry. The page renders the card image via a stateless endpoint (also not
256
- // stored) and shows the share options. `--terminal` forces the fully-offline
257
- // ASCII deck instead.
254
+ function profileName(prefetched) {
255
+ return prefetched.identity?.name || prefetched.identity?.email || "you";
256
+ }
257
+ // Render the shareable card PNG via the stateless endpoint. This is a
258
+ // server-to-server call (CLI -> Standout), so there's no browser, no CORS, and
259
+ // no cross-origin/private-network blocking. The endpoint stores nothing.
260
+ async function fetchCardPng(prefetched) {
261
+ try {
262
+ const { payload } = capPayload(prefetched);
263
+ const res = await fetch(`${STANDOUT_API_URL}/api/public/wrapped/local/share-card`, {
264
+ method: "POST",
265
+ headers: {
266
+ "Content-Type": "application/json",
267
+ "User-Agent": "Standout/0.4.0 (node)",
268
+ },
269
+ body: JSON.stringify({
270
+ profile: payload,
271
+ name: profileName(prefetched),
272
+ }),
273
+ });
274
+ if (!res.ok)
275
+ return null;
276
+ return Buffer.from(await res.arrayBuffer());
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ // Print a PNG inline using the terminal's image protocol. Returns false if the
283
+ // terminal doesn't support inline images (caller then falls back to the saved
284
+ // file path). Covers iTerm2-protocol terminals (iTerm2, WezTerm, VS Code, Hyper,
285
+ // Tabby) and Kitty-protocol terminals (Kitty, Ghostty).
286
+ function showImageInline(png) {
287
+ if (!process.stdout.isTTY)
288
+ return false;
289
+ const tp = process.env.TERM_PROGRAM || "";
290
+ const term = process.env.TERM || "";
291
+ const b64 = png.toString("base64");
292
+ const itermLike = tp === "iTerm.app" ||
293
+ tp === "WezTerm" ||
294
+ tp === "vscode" ||
295
+ tp === "Hyper" ||
296
+ tp === "Tabby" ||
297
+ process.env.LC_TERMINAL === "iTerm2";
298
+ if (itermLike) {
299
+ process.stdout.write(`\x1b]1337;File=inline=1;size=${png.length};width=44;preserveAspectRatio=1:${b64}\x07\n`);
300
+ return true;
301
+ }
302
+ const kittyLike = !!process.env.KITTY_WINDOW_ID || term.includes("kitty") || tp === "ghostty";
303
+ if (kittyLike) {
304
+ const CHUNK = 4096;
305
+ for (let i = 0; i < b64.length; i += CHUNK) {
306
+ const part = b64.slice(i, i + CHUNK);
307
+ const more = i + CHUNK < b64.length ? 1 : 0;
308
+ const ctrl = i === 0 ? `a=T,f=100,m=${more}` : `m=${more}`;
309
+ process.stdout.write(`\x1b_G${ctrl};${part}\x1b\\`);
310
+ }
311
+ process.stdout.write("\n");
312
+ return true;
313
+ }
314
+ return false;
315
+ }
316
+ // Local path: gather on-device, page the deck, then render the real shareable
317
+ // card image right in the terminal. The card is rendered on the fly by a
318
+ // stateless endpoint (nothing is saved on our servers) and saved to a temp file
319
+ // so it can be attached when sharing. `--terminal` stays fully offline (deck
320
+ // only, no network, no image).
258
321
  async function runLocal() {
259
322
  const terminalOnly = process.argv.slice(2).includes("--terminal");
260
323
  const stop = startLoadingLine("Putting together your AI highlights");
@@ -274,55 +337,33 @@ async function runLocal() {
274
337
  process.stderr.write("\n Local run complete. To publish your wrapped and see how you rank: npx standout\n\n");
275
338
  return;
276
339
  }
277
- // Then open the full wrapped (card image + share options) on localhost.
278
- // Trim the bulkiest raw fields so the in-memory body stays well under the
279
- // render endpoint's request cap, same as the upload path.
280
- const { payload } = capPayload(prefetched);
281
- const body = JSON.stringify(payload);
282
- const server = http.createServer((req, res) => {
283
- // The wrapped page (https) reads this over http://localhost, which browsers
284
- // treat as a trustworthy origin. Allow any origin for the simple GET.
285
- res.setHeader("Access-Control-Allow-Origin", "*");
286
- if (req.url && req.url.startsWith("/profile.json")) {
287
- res.setHeader("Content-Type", "application/json");
288
- res.end(body);
289
- return;
290
- }
291
- res.statusCode = 404;
292
- res.end("not found");
293
- });
294
- await new Promise((resolve, reject) => {
295
- server.on("error", reject);
296
- server.listen(0, "127.0.0.1", resolve);
297
- });
298
- const address = server.address();
299
- const port = address && typeof address === "object" ? address.port : null;
300
- if (!port) {
301
- server.close();
302
- process.stderr.write("\n Couldn't start the local server, so the browser view is unavailable.\n" +
303
- " Your wrapped is shown above. Re-run to try the browser view: npx standout --local\n\n");
340
+ const stop2 = startLoadingLine("Rendering your shareable card");
341
+ let png;
342
+ try {
343
+ png = await fetchCardPng(prefetched);
344
+ }
345
+ finally {
346
+ stop2();
347
+ }
348
+ if (!png) {
349
+ process.stderr.write("\n ✓ Your wrapped is above — that's the full local result.\n" +
350
+ " The shareable card image is generated by Standout, so it needs a\n" +
351
+ " connection. You appear to be offline; re-run online to get it.\n\n");
304
352
  return;
305
353
  }
306
- const url = `${STANDOUT_API_URL}/wrapped/live?port=${port}`;
307
- process.stderr.write(`\n Your wrapped is ready — nothing was uploaded or saved.\n` +
308
- ` ${url}\n\n` +
309
- ` Opening your browser. Keep this terminal open while you view or share.\n` +
310
- ` Press Ctrl+C when you're done.\n\n`);
311
- try {
312
- await openWrapped(url);
354
+ const file = join(tmpdir(), `standout-wrapped-${Date.now()}.png`);
355
+ writeFileSync(file, png);
356
+ process.stderr.write("\n");
357
+ const shown = showImageInline(png);
358
+ if (!shown) {
359
+ process.stderr.write(" (Your terminal can't show inline images — open the saved card below.)\n");
313
360
  }
314
- catch {
315
- process.stderr.write(` Couldn't open the browser automatically. Open this link manually:\n ${url}\n\n`);
316
- }
317
- // Hold the process open (the in-memory server keeps the data available) until
318
- // the user exits; then drop everything.
319
- await new Promise((resolve) => {
320
- process.on("SIGINT", () => {
321
- server.close();
322
- process.stderr.write("\n Closed. Nothing was saved.\n");
323
- resolve();
324
- });
325
- });
361
+ const post = encodeURIComponent("Check out my AI Wrapped 🤖 — get yours: npx standout");
362
+ process.stderr.write(`\n Saved card: ${file}\n\n` +
363
+ ` Share it:\n` +
364
+ ` X: https://x.com/intent/post?text=${post}\n` +
365
+ ` LinkedIn: https://www.linkedin.com/feed/?shareActive=true&text=${post}\n\n` +
366
+ ` (Card rendered on the fly — nothing was saved on our servers.)\n\n`);
326
367
  }
327
368
  async function runAgent(jobId) {
328
369
  // The wrapped render + share now run non-interactively (no TTY required), so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",