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.
- package/dist/cli.js +97 -56
- 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
|
|
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
|
|
64
|
+
tag: " — private",
|
|
63
65
|
lines: [
|
|
64
|
-
"
|
|
65
|
-
"
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|