usehour 0.1.0 → 0.2.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/README.md +4 -3
- package/hour.mjs +280 -58
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
Your year of building with AI, as Wrapped-style cards.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
npx usehour
|
|
6
|
+
npx usehour@0.2.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
- Asks before it reads anything, and lists the folders it will read.
|
|
10
10
|
- Reads your Claude Code and Codex session logs on your machine, read only.
|
|
11
|
-
-
|
|
11
|
+
- Counts your habits, and picks a few of your best prompts (anything that looks like a key, password, email or home folder hidden) so you can choose some for your profile.
|
|
12
12
|
- Opens your cards in your browser. Save any card as an image to post it.
|
|
13
|
-
-
|
|
13
|
+
- Optional: make a private profile on usehour.com from the cards page. You see exactly what is sent first. The script itself never connects to the internet.
|
|
14
|
+
- No dependencies, nothing installed.
|
|
14
15
|
|
|
15
16
|
It's one file: read [hour.mjs](https://usehour.com/hour.mjs) before you run it.
|
|
16
17
|
|
package/hour.mjs
CHANGED
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
/*
|
|
3
3
|
Hour Wrapped: a year of building with AI, from the sessions already on your machine.
|
|
4
4
|
|
|
5
|
-
npx
|
|
5
|
+
npx usehour (or download this file and run: node hour.mjs)
|
|
6
6
|
|
|
7
7
|
What this script does, in order:
|
|
8
8
|
1. Tells you which folders it will read, and asks before reading anything.
|
|
9
9
|
2. Reads your Claude Code (~/.claude/projects) and Codex (~/.codex/sessions)
|
|
10
10
|
session logs on this machine. Read only. Nothing is changed or deleted.
|
|
11
|
-
3.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
3. Counts: sessions, hours, models, how you brief and steer the agent.
|
|
12
|
+
It also picks a few of your strongest prompts (with anything that looks
|
|
13
|
+
like a key, token, password, email or home path already hidden) so you
|
|
14
|
+
can choose to put them on your profile.
|
|
15
|
+
4. Writes ./hour-wrapped/index.html (your cards) and wrapped.json (the
|
|
16
|
+
counts only), then opens the cards in your browser.
|
|
17
|
+
5. This script never uses the network. The cards page can make one request,
|
|
18
|
+
to usehour.com, and only when you press "Create my private profile" after
|
|
19
|
+
seeing exactly what will be sent. The page is locked so it cannot contact
|
|
20
|
+
anything else.
|
|
18
21
|
|
|
19
|
-
No dependencies. No install step.
|
|
22
|
+
No dependencies. No install step. Run with --no-share for cards only.
|
|
20
23
|
Source: https://usehour.com/hour.mjs Questions: lush@usehour.com
|
|
21
24
|
*/
|
|
22
25
|
import fs from "node:fs";
|
|
@@ -27,8 +30,8 @@ import { spawn } from "node:child_process";
|
|
|
27
30
|
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
28
31
|
import { fileURLToPath } from "node:url";
|
|
29
32
|
|
|
30
|
-
const VERSION = "0.
|
|
31
|
-
const
|
|
33
|
+
const VERSION = "0.2.0";
|
|
34
|
+
const API_DEFAULT = "https://usehour.com/api";
|
|
32
35
|
|
|
33
36
|
/* ------------------------------------------------------------------ patterns */
|
|
34
37
|
const TEST = /\b(pytest|vitest|jest|mocha|playwright|go test|cargo test|npm (run )?test|pnpm (run )?test|yarn test|bun test|node --test|npm run (check|lint|typecheck|build)|tsc\b|astro check|ruff|eslint|rspec|phpunit|dotnet test|mvn test|gradle test|--check\b)/i;
|
|
@@ -37,6 +40,43 @@ const CORRECT = /^(no\b|nope|not\b|wait\b|stop\b|don'?t\b|do not\b|actually\b|in
|
|
|
37
40
|
const GOAHEAD = /^(ok(ay)?|all right|alright|yes|yup|yep|sure|great|cool|nice|perfect|good|please proceed|proceed|go ahead|continue|do it|let'?s go)\b/i;
|
|
38
41
|
const QUESTION = /^(what|why|how|where|which|who|can you|could you|is it|are you|do you|does)\b/i;
|
|
39
42
|
const DONTKNOW = /\bi don'?t know\b/i;
|
|
43
|
+
/* How someone briefs an agent. Each is a yes/no per prompt. */
|
|
44
|
+
const CTX = /\b(because|so that|the goal|goal is|goal:|context|background|currently|right now|we'?re building|i'?m building|we have an?|the problem|the issue is|users?|customers?|the idea is)\b/i;
|
|
45
|
+
const LIM = /\b(don'?t|do not|must|never|only|without|avoid|keep (it|the|this)|no more than|at most|should ?n[o']t|make sure|constraints?)\b/i;
|
|
46
|
+
const FIN = /\b(done when|when (you'?re|you are) done|should (be able|return|show|work|pass|look|be)|expected|acceptance|success|until|end result|output should|deliverable|so i can|report back|let me know|the result)\b/i;
|
|
47
|
+
const SEQ = /\b(first|then|after that|next|finally|once (that|this|it)'?s? (is )?done)\b/gi;
|
|
48
|
+
const LISTLINE = /^\s*(\d+[.)]|[-*•]|step \d+)\s+\S/gim;
|
|
49
|
+
const SPECIFIC = /`[^`\n]+`|\b[\w./-]+\.(tsx?|jsx?|mjs|py|go|rs|rb|java|kt|swift|md|json|css|html|astro|vue|svelte|sql|ya?ml|toml|sh|env)\b|https?:\/\/\S+|\b[a-z]+[A-Z][A-Za-z0-9]+\b|\b[a-z0-9]+_[a-z0-9_]+\b|\bline \d+|\b\d+(px|ms|s|kb|mb|%)\b/;
|
|
50
|
+
const EXPLAIN = /\b(because|instead|should|expected|the (error|issue|problem|bug)|it (shows|says|returns|still)|still|wrong|broke|missing)\b/i;
|
|
51
|
+
/* Text copied from an agent's own output rather than written by you. */
|
|
52
|
+
const PASTED = /\b(Ran|Read|Edited|Wrote|Searched) \d+ (commands?|files?|lines?|patterns?)\b|[⏺⎿✻]|^\s*(Thinking|Thought for \d+)/m;
|
|
53
|
+
const CODEY = /[{};]\s*$|^\s{4,}\S|^\s*(at |import |from |const |let |def |function |class |return |\$ |>>> |File ")|^\s*[<{[]/;
|
|
54
|
+
|
|
55
|
+
/* Anything that looks secret or personal is replaced before a prompt leaves
|
|
56
|
+
the file parser. This runs on the few prompts kept as examples; the rest
|
|
57
|
+
are never kept at all. */
|
|
58
|
+
const SECRETS = [
|
|
59
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----|$)/g,
|
|
60
|
+
/\b(sk|pk|rk)-(ant-|proj-|live_|test_)?[A-Za-z0-9_-]{16,}/g,
|
|
61
|
+
/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
62
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
|
|
63
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
64
|
+
/\bAIza[0-9A-Za-z_-]{30,}/g,
|
|
65
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
66
|
+
/\b(bearer|basic)\s+[A-Za-z0-9._~+\/=-]{12,}/gi,
|
|
67
|
+
/\b(api[_-]?key|secret|token|password|passwd|pwd|auth|credential|private[_-]?key|access[_-]?key)(\s*[:=]\s*|\s+is\s+)["']?[^\s"',;]{4,}/gi,
|
|
68
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@\/]+:[^\s@\/]+@/gi,
|
|
69
|
+
/\b[A-Fa-f0-9]{32,}\b/g,
|
|
70
|
+
/\b[A-Za-z0-9+\/_-]{40,}={0,2}/g,
|
|
71
|
+
/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
72
|
+
/\+?\b\d[\d ()-]{8,}\d\b/g,
|
|
73
|
+
/\b\d{1,3}(\.\d{1,3}){3}\b/g,
|
|
74
|
+
];
|
|
75
|
+
function redact(t) {
|
|
76
|
+
let out = t.replace(/(\/Users\/|\/home\/|[A-Z]:\\Users\\)[^\/\\\s]+/g, "~");
|
|
77
|
+
for (const re of SECRETS) out = out.replace(re, "[hidden]");
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
/* ------------------------------------------------------------ file parsing */
|
|
42
82
|
/* One file in, one small summary out. Prompt text stays inside this function:
|
|
@@ -51,14 +91,29 @@ function shortKey(t) {
|
|
|
51
91
|
}
|
|
52
92
|
function promptFacts(ts, text) {
|
|
53
93
|
const t = text.trim();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
const words = t.split(/\s+/).filter(Boolean).length;
|
|
95
|
+
const lines = t.split("\n").filter((l) => l.trim());
|
|
96
|
+
const codey = lines.length >= 4 && lines.filter((l) => CODEY.test(l)).length / lines.length > 0.35;
|
|
97
|
+
const kind = CORRECT.test(t) ? "correct" : (GOAHEAD.test(t) && words <= 12) ? "go" : (t.endsWith("?") || QUESTION.test(t)) ? "question" : "direct";
|
|
98
|
+
const f = {
|
|
99
|
+
ts, words, kind, codey,
|
|
57
100
|
verify: VERIFY.test(t),
|
|
58
101
|
dontknow: DONTKNOW.test(t),
|
|
59
|
-
kind: CORRECT.test(t) ? "correct" : (GOAHEAD.test(t) && t.split(/\s+/).length <= 12) ? "go" : (t.endsWith("?") || QUESTION.test(t)) ? "question" : "direct",
|
|
60
102
|
short: shortKey(t),
|
|
103
|
+
ctx: CTX.test(t), lim: LIM.test(t), fin: FIN.test(t),
|
|
104
|
+
steps: (t.match(LISTLINE) || []).length >= 2 || (t.match(SEQ) || []).length >= 2,
|
|
105
|
+
spec: SPECIFIC.test(t),
|
|
106
|
+
explains: kind === "correct" && words >= 12 && EXPLAIN.test(t),
|
|
107
|
+
pasted: PASTED.test(t),
|
|
61
108
|
};
|
|
109
|
+
return f;
|
|
110
|
+
}
|
|
111
|
+
/* How good an example a prompt is: a clear brief with context, limits, a
|
|
112
|
+
finish line, steps, specifics and a request for proof. Long pastes of code
|
|
113
|
+
or logs are not examples of prompting, so they are skipped. */
|
|
114
|
+
function exampleScore(f, first) {
|
|
115
|
+
if (f.codey || f.pasted || f.words < 30 || f.words > 700) return 0;
|
|
116
|
+
return f.ctx + f.lim + f.fin + f.steps + f.verify + 0.5 * f.spec + Math.min(1, f.words / 120) + (first ? 0.5 : 0);
|
|
62
117
|
}
|
|
63
118
|
function codexUserText(p) {
|
|
64
119
|
const pick = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((x) => x && (x.text || x.input_text) || "").filter((x) => typeof x === "string").join("\n") : "";
|
|
@@ -75,10 +130,11 @@ function cleanPrompt(t) {
|
|
|
75
130
|
if (!t || t.startsWith("<") || t.startsWith("# AGENTS.md") || t.startsWith("Caveat:") || t.startsWith("[Request interrupted") || t.startsWith("# Files mentioned")) return null;
|
|
76
131
|
return t;
|
|
77
132
|
}
|
|
133
|
+
let KEEP_EXAMPLES = true;
|
|
78
134
|
const KEEP_CODEX = /"(session_meta|turn_context|user_message|UserMessage|function_call|custom_tool_call|local_shell_call|token_count|turn_aborted)"|"role":\s*"user"/;
|
|
79
135
|
const KEEP_CLAUDE = /"type":\s*"(user|assistant)"/;
|
|
80
136
|
async function parseFile(file, src) {
|
|
81
|
-
const s = { src, sub: file.includes(`${path.sep}subagents${path.sep}`), start: null, end: null, cwd: null, models: {}, prompts: [], ids: [], patches: 0, cmds: 0, tests: 0, aborts: 0, tokens: 0 };
|
|
137
|
+
const s = { src, sub: file.includes(`${path.sep}subagents${path.sep}`), start: null, end: null, cwd: null, models: {}, prompts: [], ids: [], examples: [], patches: 0, cmds: 0, tests: 0, aborts: 0, tokens: 0 };
|
|
82
138
|
const seenMsg = new Set(); const seenText = new Set();
|
|
83
139
|
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
84
140
|
for await (const line of rl) {
|
|
@@ -98,7 +154,7 @@ async function parseFile(file, src) {
|
|
|
98
154
|
if (text && text.trim().startsWith("[Request interrupted")) s.aborts++;
|
|
99
155
|
if (o.isMeta) continue;
|
|
100
156
|
const u = cleanPrompt(text);
|
|
101
|
-
if (u) {
|
|
157
|
+
if (u) { const f = promptFacts(ts, u); keepExample(s, f, u); s.prompts.push(f); s.ids.push(o.uuid || null); }
|
|
102
158
|
} else if (o.type === "assistant") {
|
|
103
159
|
if (m.id && !seenMsg.has(m.id)) {
|
|
104
160
|
seenMsg.add(m.id);
|
|
@@ -127,7 +183,7 @@ async function parseFile(file, src) {
|
|
|
127
183
|
const ut = codexUserText(p);
|
|
128
184
|
if (ut !== null && (o.type === "event_msg" || o.type === "response_item")) {
|
|
129
185
|
const u = cleanPrompt(ut);
|
|
130
|
-
if (u && !seenText.has(u)) { seenText.add(u);
|
|
186
|
+
if (u && !seenText.has(u)) { seenText.add(u); const f = promptFacts(ts, u); keepExample(s, f, u); s.prompts.push(f); s.ids.push(null); }
|
|
131
187
|
}
|
|
132
188
|
if (o.type === "response_item" && ["function_call", "custom_tool_call", "local_shell_call"].includes(p.type)) {
|
|
133
189
|
const name = p.name || "";
|
|
@@ -140,10 +196,21 @@ async function parseFile(file, src) {
|
|
|
140
196
|
}
|
|
141
197
|
return s;
|
|
142
198
|
}
|
|
199
|
+
/* Keeps the best three example prompts of a file, secrets hidden. */
|
|
200
|
+
function keepExample(s, f, text) {
|
|
201
|
+
if (!KEEP_EXAMPLES) return;
|
|
202
|
+
const q = exampleScore(f, s.prompts.length === 0);
|
|
203
|
+
if (q < 3) return;
|
|
204
|
+
if (s.examples.length >= 3 && q <= s.examples[2].q) return;
|
|
205
|
+
const clean = redact(text).slice(0, 2400);
|
|
206
|
+
s.examples.push({ q: clean.includes("[hidden]") ? q - 0.5 : q, text: clean, ts: f.ts, words: f.words });
|
|
207
|
+
s.examples.sort((a, b) => b.q - a.q); s.examples.length = Math.min(3, s.examples.length);
|
|
208
|
+
}
|
|
143
209
|
|
|
144
210
|
/* Worker threads parse files in parallel; each returns only its summary. */
|
|
145
211
|
if (!isMainThread) {
|
|
146
|
-
parentPort.on("message", async ({ file, src }) => {
|
|
212
|
+
parentPort.on("message", async ({ file, src, examples }) => {
|
|
213
|
+
KEEP_EXAMPLES = examples !== false;
|
|
147
214
|
try { parentPort.postMessage({ ok: true, s: await parseFile(file, src) }); }
|
|
148
215
|
catch (e) { parentPort.postMessage({ ok: false, error: String(e && e.message || e) }); }
|
|
149
216
|
});
|
|
@@ -206,6 +273,20 @@ function compute(summaries) {
|
|
|
206
273
|
const goto = Object.entries(shorts).filter(([, c]) => c >= 5).sort((a, b) => b[1] - a[1])[0] || null;
|
|
207
274
|
const days = new Set(P.map((p) => p.d.toDateString())).size;
|
|
208
275
|
const projects = new Set(main.map((s) => s.cwd).filter(Boolean)).size;
|
|
276
|
+
/* How you prompt: five habits, each the share of prompts where it shows.
|
|
277
|
+
"notable" is the level where the habit stands out; the bar fills there. */
|
|
278
|
+
const long = P.filter((p) => p.words >= 25 && !p.codey), mid = P.filter((p) => p.words >= 8 && !p.codey);
|
|
279
|
+
const briefs = P.filter((p) => p.i === 0 && !p.codey);
|
|
280
|
+
const corrections = follows.filter((p) => p.kind === "correct");
|
|
281
|
+
const rate = (a, f) => (a.length ? a.filter(f).length / a.length : 0);
|
|
282
|
+
const habit = (key, label, value, notable, base, what) => ({ key, label, value, notable, base, what });
|
|
283
|
+
const promptProfile = [
|
|
284
|
+
habit("brief", "Briefing", rate(briefs, (p) => p.words >= 30 && p.ctx + p.lim + p.fin >= 2), 0.4, briefs.length, "opening prompts that give context, limits or a finish line (at least two)"),
|
|
285
|
+
habit("steps", "Breaking it down", rate(long, (p) => p.steps), 0.25, long.length, "longer prompts laid out as steps"),
|
|
286
|
+
habit("proof", "Asking for proof", main.length ? verifySessions / main.length : 0, 0.45, main.length, "sessions that asked the agent to test, verify or screenshot"),
|
|
287
|
+
habit("specific", "Being specific", rate(mid, (p) => p.spec), 0.35, mid.length, "prompts that name files, functions, errors or numbers"),
|
|
288
|
+
habit("recover", "Correcting clearly", rate(corrections, (p) => p.explains), 0.5, corrections.length, "corrections that say what is wrong or what to do instead"),
|
|
289
|
+
].filter((h) => h.base >= 10);
|
|
209
290
|
const first = new Date(Math.min(...P.map((p) => p.d.getTime()))), last = new Date(Math.max(...P.map((p) => p.d.getTime())));
|
|
210
291
|
return {
|
|
211
292
|
version: VERSION,
|
|
@@ -223,8 +304,25 @@ function compute(summaries) {
|
|
|
223
304
|
goToPrompt: goto ? { text: goto[0], count: goto[1] } : null,
|
|
224
305
|
dontKnow: P.filter((p) => p.dontknow).length,
|
|
225
306
|
tokens: summaries.reduce((a, s) => a + s.tokens, 0),
|
|
307
|
+
promptProfile,
|
|
226
308
|
};
|
|
227
309
|
}
|
|
310
|
+
/* The strongest example prompts across all sessions: no repeats, at most two
|
|
311
|
+
from any one project, best first. They stay on this computer unless you
|
|
312
|
+
choose some for your profile. */
|
|
313
|
+
function pickExamples(summaries, n = 12) {
|
|
314
|
+
const all = summaries.filter((s) => !s.sub).flatMap((s) => (s.examples || []).map((e) => ({ ...e, cwd: s.cwd || "" })));
|
|
315
|
+
all.sort((a, b) => b.q - a.q);
|
|
316
|
+
const seen = new Set(), per = {}, out = [];
|
|
317
|
+
for (const e of all) {
|
|
318
|
+
const k = e.text.toLowerCase().replace(/\s+/g, " ").slice(0, 200);
|
|
319
|
+
if (seen.has(k) || (per[e.cwd] || 0) >= 2) continue;
|
|
320
|
+
seen.add(k); per[e.cwd] = (per[e.cwd] || 0) + 1;
|
|
321
|
+
out.push({ text: e.text, words: e.words, date: (e.ts || "").slice(0, 10) });
|
|
322
|
+
if (out.length >= n) break;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
228
326
|
|
|
229
327
|
/* Builder types. Each signal is scored against the level where it becomes
|
|
230
328
|
notable; the highest score wins, and below 1 everywhere you are a Pair
|
|
@@ -253,6 +351,9 @@ function builderType(m) {
|
|
|
253
351
|
const monthYear = (d) => new Date(d + "T12:00:00Z").toLocaleString("en", { month: "short", year: "numeric" });
|
|
254
352
|
const hourLabel = (h) => (h % 12 || 12) + (h < 12 ? " AM" : " PM");
|
|
255
353
|
const compact = (n) => n >= 1e9 ? (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "K" : String(n);
|
|
354
|
+
/* Dots for a habit: reaching the notable level fills 7 of 10, the last 3 are
|
|
355
|
+
for going beyond it. */
|
|
356
|
+
const habitFill = (h) => Math.min(1, 0.7 * Math.min(1, h.value / h.notable) + 0.3 * Math.max(0, (h.value - h.notable) / (1 - h.notable)));
|
|
256
357
|
function cards(m, name) {
|
|
257
358
|
const type = TYPES[builderType(m)];
|
|
258
359
|
const tools = [m.sessions.claudeCode && "Claude Code", m.sessions.codex && "Codex"].filter(Boolean).join(" and ");
|
|
@@ -273,15 +374,149 @@ function cards(m, name) {
|
|
|
273
374
|
if (m.dontKnow >= 10) C.push({ id: "honest", bg: "#FB65C2", fg: "#0B0B0A", dot: "#0B0B0A", pat: "rain", ask: "Your most honest phrase", big: "“I don't know”", line: `${m.dontKnow} times. You think out loud, with the agent.`, bs: "14cqw" });
|
|
274
375
|
if (m.tokens > 0) C.push({ id: "tokens", bg: "#2B5FE3", fg: "#FFFFFF", dot: "#FFFFFF", pat: "field", ask: "How much you built", big: compact(m.tokens), line: "tokens processed.", bs: "30cqw" });
|
|
275
376
|
if (m.proof.codingSessions >= 5) C.push({ id: "edge", bg: "#F7F3EC", fg: "#0B0B0A", dot: "#F2601F", pat: "ring", pct: tested, ask: tested < 0.75 ? "Your growth edge" : "Your strength", big: `${pct(tested)}%`, line: tested < 0.75 ? "of coding sessions ran a test. Push that higher." : "of coding sessions ran a test. Keep it that way.", bs: "30cqw" });
|
|
377
|
+
if (m.promptProfile && m.promptProfile.length >= 3) {
|
|
378
|
+
const hp = m.promptProfile, best = [...hp].sort((a, b) => b.value / b.notable - a.value / a.notable)[0];
|
|
379
|
+
C.push({ id: "prompting", bg: "#F7F3EC", fg: "#0B0B0A", bs: "11cqw", rows: hp.map((h) => [h.label, habitFill(h), `${pct(h.value)}%`]), ask: "How you prompt", big: best.label, line: `Your strongest habit: ${pct(best.value)}% of ${best.what}.` });
|
|
380
|
+
}
|
|
276
381
|
C.push({ id: "summary", bg: "#0B0B0A", fg: "#F4F1EA", summary: true, who: name || "", period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, type: type.name,
|
|
277
382
|
stats: [["Sessions", m.sessions.total.toLocaleString("en")], ["Busiest hour", hourLabel(m.busiestHour)], ["Go-to prompt", m.goToPrompt ? `“${m.goToPrompt.text}”` : "–"], ["Agents at once", String(m.parallelMax)], ["Made it prove it", `${pct(verify)}%`], ["Model", top ? top.name.replace(/^Claude /, "") : "–"]] });
|
|
278
383
|
return C;
|
|
279
384
|
}
|
|
280
385
|
|
|
386
|
+
/* ------------------------------------------------------- the claim flow
|
|
387
|
+
Shown only when sharing is on. It runs in the cards page on your computer.
|
|
388
|
+
The page talks to usehour.com twice at most, each time because you pressed
|
|
389
|
+
a button: "Continue" checks that your handle is free (sends the handle
|
|
390
|
+
only), "Create" sends the profile you saw in step 3. */
|
|
391
|
+
const CLAIM_CSS = String.raw`
|
|
392
|
+
.claimc .cdots{margin-top:4cqw;height:14cqw;background-image:radial-gradient(circle,rgba(244,241,234,.95) 0 .95cqw,transparent 1.05cqw);background-size:3.6cqw 3.6cqw;-webkit-mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%);mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%)}
|
|
393
|
+
.claimc .pill{margin-top:4cqw;align-self:flex-start;padding:2cqw 3.6cqw;border-radius:999px;background:rgba(244,241,234,.1);box-shadow:inset 0 0 0 .3cqw rgba(244,241,234,.22);font:4.2cqw var(--mono)}.claimc .pill span{opacity:.6}.claimc .pill b{font-weight:500}
|
|
394
|
+
.claimc .cta{position:relative;z-index:5;margin-top:5cqw;height:13cqw;border-radius:999px;border:0;background:#F4F1EA;color:#0B0B0A;font:600 4.6cqw var(--sans);cursor:pointer}
|
|
395
|
+
.claimbar{margin-top:28px;display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;background:var(--ink);color:#F4F1EA;border-radius:22px;padding:22px 24px}.claimbar b{display:block;font-size:20px;letter-spacing:-.02em}.claimbar span{font-size:14px;opacity:.8}
|
|
396
|
+
.claimbar button{height:48px;padding:0 22px;border-radius:999px;border:0;background:#F4F1EA;color:var(--ink);font:600 15px var(--sans);cursor:pointer}
|
|
397
|
+
.cl{position:fixed;inset:0;z-index:50;background:rgba(11,11,10,.55);display:flex;align-items:center;justify-content:center;padding:16px}.cl[hidden]{display:none}
|
|
398
|
+
.cl-sheet{width:100%;max-width:420px;height:min(860px,calc(100svh - 32px));background:var(--paper);border-radius:28px;display:flex;flex-direction:column;padding:18px 24px 22px;overflow:auto}
|
|
399
|
+
@media(max-width:520px){.cl{padding:0}.cl-sheet{max-width:none;height:100svh;border-radius:0}}
|
|
400
|
+
.cl-top{flex:none;display:flex;justify-content:space-between;align-items:center;font-size:14px;color:var(--ink-3)}.cl-back{background:none;border:0;font:14px var(--sans);color:#3A3935;padding:10px 0;cursor:pointer;min-height:44px}
|
|
401
|
+
.cl-prog{flex:none;display:flex;gap:6px;margin-top:4px}.cl-prog i{flex:1;height:4px;border-radius:2px;background:rgba(11,11,10,.12)}.cl-prog i.on{background:var(--ink)}
|
|
402
|
+
.cl-h{flex:none;margin:24px 0 0;font-size:34px;line-height:1.02;letter-spacing:-.045em;font-weight:650}
|
|
403
|
+
.cl-p{flex:none;margin:10px 0 0;font-size:15px;line-height:1.45;color:#3A3935}
|
|
404
|
+
.cl-hcard{flex:none;margin-top:22px;background:var(--ink);color:#F4F1EA;border-radius:22px;padding:20px;position:relative;overflow:hidden}
|
|
405
|
+
.cl-hcard:before{content:"";position:absolute;right:-10px;top:-10px;width:170px;height:120px;background-image:radial-gradient(circle,rgba(244,241,234,.9) 0 2.6px,transparent 3px);background-size:12px 12px;-webkit-mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 70%);mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 70%)}
|
|
406
|
+
.cl-hcard label{position:relative;font-size:13px;opacity:.7}
|
|
407
|
+
.cl-hrow{position:relative;display:flex;align-items:baseline;margin-top:4px}.cl-hrow span{font-size:40px;font-weight:650;letter-spacing:-.05em}
|
|
408
|
+
.cl-hrow input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:#F4F1EA;font:650 40px var(--sans);letter-spacing:-.05em;padding:0}
|
|
409
|
+
.cl-hst{position:relative;margin-top:8px;font-size:14px;min-height:20px;opacity:.85}
|
|
410
|
+
.cl-f{flex:none;display:flex;flex-direction:column;gap:6px;margin-top:14px;font-size:14px;font-weight:500}
|
|
411
|
+
.cl-f input{height:48px;padding:0 14px;border-radius:12px;border:1px solid rgba(11,11,10,.14);background:#fff;font:16px var(--sans);color:var(--ink)}
|
|
412
|
+
.cl-fine{flex:none;margin:10px 0 0;font-size:13px;line-height:1.45;color:var(--ink-3)}.cl-fine.c{text-align:center}
|
|
413
|
+
.cl-err{flex:none;margin-top:12px;font-size:14px;color:#B3261E;font-weight:500}.cl-err:empty{display:none}
|
|
414
|
+
.cl-sp{flex:1 0 16px}
|
|
415
|
+
.cl-go{flex:none;height:56px;border-radius:999px;border:0;background:var(--ink);color:var(--paper);font:600 17px var(--sans);cursor:pointer;width:100%}.cl-go[disabled]{opacity:.5;cursor:wait}
|
|
416
|
+
.cl-skip{flex:none;display:block;width:100%;height:44px;background:none;border:0;font:14px var(--sans);color:var(--ink-3);cursor:pointer}
|
|
417
|
+
.cl-lw{position:relative;margin-top:16px;flex:1 1 auto;min-height:180px;display:flex;flex-direction:column}
|
|
418
|
+
.cl-list{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px;padding:3px 3px 48px}
|
|
419
|
+
.cl-pc{flex:none;border-radius:20px;padding:16px;background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.12);cursor:pointer}
|
|
420
|
+
.cl-pc.on{box-shadow:0 0 0 3px var(--ink)}
|
|
421
|
+
.cl-pch{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px;font-weight:500}.cl-pch span:first-child{opacity:.8}
|
|
422
|
+
.cl-pick{flex:none;border-radius:999px;padding:5px 11px;box-shadow:inset 0 0 0 1px rgba(11,11,10,.22)}.cl-pc.on .cl-pick{background:var(--ink);color:#fff;box-shadow:none}
|
|
423
|
+
.cl-pc p{margin:10px 0 0;font-size:16px;line-height:1.32;letter-spacing:-.015em;font-weight:500;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-line}
|
|
424
|
+
.cl-pc textarea{display:block;width:100%;margin-top:10px;min-height:180px;border:0;border-radius:12px;padding:10px 12px;font:15px/1.45 var(--sans);background:rgba(255,255,255,.94);color:var(--ink);resize:vertical}
|
|
425
|
+
.cl-edit{margin-top:6px;background:none;border:0;padding:8px 0;font:600 13px var(--sans);color:inherit;text-decoration:underline;cursor:pointer}
|
|
426
|
+
.hid{background:var(--ink);color:var(--ink);border-radius:5px;padding:0 4px}
|
|
427
|
+
.cl-more{position:absolute;left:0;right:0;bottom:0;height:90px;background:linear-gradient(180deg,rgba(247,243,236,0),var(--paper) 72%);display:flex;align-items:flex-end;justify-content:center;pointer-events:none;padding-bottom:4px}.cl-more[hidden]{display:none}
|
|
428
|
+
.cl-more span{display:inline-flex;align-items:center;height:32px;padding:0 14px;border-radius:999px;background:#fff;box-shadow:0 4px 14px rgba(11,11,10,.08),inset 0 0 0 1px rgba(11,11,10,.1);font-size:13px;font-weight:500;color:#3A3935}
|
|
429
|
+
.cl-opt{flex:none;display:block;border-radius:20px;padding:18px;margin-top:12px;cursor:pointer;background:#fff;box-shadow:inset 0 0 0 1px rgba(11,11,10,.1)}.cl-opt.dk{background:var(--ink);color:#F4F1EA;box-shadow:none}
|
|
430
|
+
.cl-oh{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:18px;font-weight:650;letter-spacing:-.02em}
|
|
431
|
+
.cl-oh input{width:24px;height:24px;flex:none;accent-color:var(--ink)}.cl-opt.dk .cl-oh input{accent-color:#B0ACFE}
|
|
432
|
+
.cl-ot{display:block;margin-top:8px;font-size:15px;line-height:1.4;color:#3A3935}.cl-opt.dk .cl-ot{color:#F4F1EA;opacity:.9}.cl-opt.dk .cl-ot.s{font-size:13px;opacity:.65}
|
|
433
|
+
.cl-ul{margin:10px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px;font-size:15px;line-height:1.4;color:#3A3935}.cl-ul li{display:flex;gap:8px}.cl-ul li:before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dot);margin-top:8px}
|
|
434
|
+
.cl-h3{flex:none;margin:26px 0 10px;font-size:18px;letter-spacing:-.02em}
|
|
435
|
+
.cl-two{flex:none;display:grid;grid-template-columns:1fr 1fr;gap:10px}.cl-two div{background:#fff;border-radius:16px;padding:14px;box-shadow:inset 0 0 0 1px rgba(11,11,10,.1);font-size:14px;line-height:1.35}.cl-two b{display:flex;align-items:center;gap:7px;font-size:13px;margin-bottom:4px}.cl-two b i{width:9px;height:9px;border-radius:50%}.cl-two span{display:block;margin-top:6px}
|
|
436
|
+
.cl-link{flex:none;align-self:flex-start;background:none;border:0;padding:10px 0;font:13px var(--sans);color:var(--ink-3);text-decoration:underline;cursor:pointer}
|
|
437
|
+
.cl pre{flex:none;background:var(--ink);color:#F4F1EA;border-radius:14px;padding:14px;font:12px/1.5 var(--mono);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;margin:0 0 12px}
|
|
438
|
+
.cl-done{flex:none;margin-top:28px;background:var(--ink);color:#F4F1EA;border-radius:24px;padding:28px 24px;position:relative;overflow:hidden}
|
|
439
|
+
.cl-done:before{content:"";position:absolute;inset:0 0 auto auto;width:100%;height:120px;background-image:radial-gradient(circle,rgba(244,241,234,.9) 0 2.6px,transparent 3px);background-size:12px 12px;-webkit-mask-image:linear-gradient(200deg,#000,transparent 70%);mask-image:linear-gradient(200deg,#000,transparent 70%)}
|
|
440
|
+
.cl-done h2{position:relative;margin:90px 0 0;font-size:44px;line-height:.95;letter-spacing:-.05em;font-weight:650}
|
|
441
|
+
.cl-done a.u{position:relative;display:block;margin-top:16px;font:15px var(--mono);color:#F4F1EA;word-break:break-all}
|
|
442
|
+
.cl-done p{position:relative;margin:14px 0 0;font-size:14px;line-height:1.45;opacity:.8}
|
|
443
|
+
.cl-row{flex:none;display:flex;gap:10px;margin-top:16px}.cl-row a,.cl-row button{flex:1;display:flex;align-items:center;justify-content:center;height:52px;border-radius:999px;font:600 16px var(--sans);text-decoration:none;cursor:pointer;border:0}
|
|
444
|
+
.cl-row a{background:var(--ink);color:var(--paper)}.cl-row button{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}
|
|
445
|
+
`;
|
|
446
|
+
const CLAIM_HTML = `<div class="claimbar" id="claimbar"><div><b>Claim your handle</b><span>Turn these cards into your profile at usehour.com/@you.</span></div><button type="button" id="claimb">Claim your handle</button></div>
|
|
447
|
+
<div class="cl" id="cl" hidden role="dialog" aria-modal="true" aria-label="Claim your handle"><div class="cl-sheet" id="cls"></div></div>`;
|
|
448
|
+
const CLAIM_JS = String.raw`
|
|
449
|
+
var CS={step:1,handle:D.suggest||"",name:"",email:"",picked:[],texts:(D.examples||[]).map(function(e){return e.text}),editing:-1,pub:false,roles:false,feature:false,busy:false,err:"",data:false,done:null};
|
|
450
|
+
var HANDLE_RE=/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,23}$/,EMAIL_RE=/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
|
|
451
|
+
var PCOL=[["#DBA004","#0B0B0A"],["#8B5FD9","#FFFFFF"],["#2B5FE3","#FFFFFF"]];
|
|
452
|
+
function hidHTML(t){return esc(t).replace(/\[hidden\]/g,'<span class="hid">hidden</span>')}
|
|
453
|
+
function claimEl(){var d=document.createElement("article");d.className="card claimc";d.style.setProperty("--bg","#0B0B0A");d.style.setProperty("--fg","#F4F1EA");d.style.setProperty("--bs","15cqw");
|
|
454
|
+
var h=CS.handle||"you";d.innerHTML='<div class="in"><div class="ask">One more thing</div><div class="cdots"></div><div class="big">Claim your handle.</div><div class="pill"><span>usehour.com/</span><b class="grad">@'+esc(h)+'</b></div><div class="line">These cards become your profile. Handles are first come, first served.</div><button type="button" class="cta">Claim @'+esc(h)+'</button><div class="ft"><span class="b"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</span><span class="u">usehour.com</span></div></div>';
|
|
455
|
+
d.querySelector(".cta").onclick=function(e){e.stopPropagation();openClaim()};return d}
|
|
456
|
+
function openClaim(){document.getElementById("cl").hidden=false;document.body.style.overflow="hidden";renderClaim()}
|
|
457
|
+
function closeClaim(){document.getElementById("cl").hidden=true;document.body.style.overflow=""}
|
|
458
|
+
function payload(){return{v:2,handle:CS.handle,name:CS.name.trim(),email:CS.email.trim(),linkedin:"",public:CS.pub,roles:CS.roles,feature:CS.feature,prompts:CS.picked.map(function(i){return{text:CS.texts[i].trim(),date:D.examples[i].date}}).filter(function(p){return p.text}),wrapped:D.m}}
|
|
459
|
+
function prog(n){return'<div class="cl-top"><button type="button" class="cl-back" data-a="back">'+(n===1?"Close":"Back")+'</button><span>'+n+' of 3</span></div><div class="cl-prog">'+[1,2,3].map(function(i){return'<i'+(i<=n?' class="on"':'')+'></i>'}).join("")+'</div>'}
|
|
460
|
+
function handleNote(){var h=CS.handle;if(!h)return"Letters, numbers and dashes.";if(!HANDLE_RE.test(h))return"3 to 24 letters, numbers or single dashes.";return"Press Continue to check it's free."}
|
|
461
|
+
function renderClaim(){var s=document.getElementById("cls"),H="",n=CS.step;
|
|
462
|
+
if(n===1){H=prog(1)+'<h2 class="cl-h">Pick your handle.</h2><p class="cl-p">It’s the address of your profile. Once it’s yours, nobody else can have it.</p>'
|
|
463
|
+
+'<div class="cl-hcard"><label for="clh">usehour.com/</label><div class="cl-hrow"><span class="grad">@</span><input id="clh" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="24" value="'+esc(CS.handle)+'"></div><div class="cl-hst" id="clhst">'+esc(handleNote())+'</div></div>'
|
|
464
|
+
+'<label class="cl-f">Your name<input id="cln" autocomplete="name" maxlength="80" value="'+esc(CS.name)+'"></label>'
|
|
465
|
+
+'<label class="cl-f">Email, to keep your handle<input id="cle" type="email" autocomplete="email" maxlength="200" value="'+esc(CS.email)+'"></label>'
|
|
466
|
+
+'<p class="cl-fine">We email you a link to your profile. That’s how you get back in. No password.</p><div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div>'
|
|
467
|
+
+'<button type="button" class="cl-go" data-a="next1"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Checking…":"Continue")+'</button><p class="cl-fine c">Continue checks the name with usehour.com. Nothing else is sent.</p>'}
|
|
468
|
+
else if(n===2){var k=CS.picked.length;H=prog(2)+'<h2 class="cl-h">Show how you brief an agent.</h2><p class="cl-p">Your strongest prompts, found on your laptop. Pick up to 3. Edit out anything private first.</p>'
|
|
469
|
+
+'<div class="cl-lw"><div class="cl-list" id="cll">'+D.examples.map(function(e,i){var at=CS.picked.indexOf(i),on=at>=0,c=on?PCOL[at%3]:null,ed=CS.editing===i,w=CS.texts[i].split(/\s+/).filter(Boolean).length,hidN=(CS.texts[i].match(/\[hidden\]/g)||[]).length;
|
|
470
|
+
return'<div class="cl-pc'+(on?" on":"")+'" data-a="pick" data-i="'+i+'"'+(c?' style="background:'+c[0]+';color:'+c[1]+'"':'')+'><div class="cl-pch"><span>'+esc(e.date?new Date(e.date+"T12:00:00Z").toLocaleString("en",{month:"short",day:"numeric",timeZone:"UTC"})+" · ":"")+w+' words'+(hidN?" · "+hidN+" hidden":"")+'</span><span class="cl-pick">'+(on?"✓ Picked":"Pick")+'</span></div>'
|
|
471
|
+
+(ed?'<textarea data-i="'+i+'" aria-label="Edit this prompt">'+esc(CS.texts[i])+'</textarea>':'<p>“'+hidHTML(CS.texts[i])+'”</p>')
|
|
472
|
+
+'<button type="button" class="cl-edit" data-a="edit" data-i="'+i+'">'+(ed?"Done editing":"Edit")+'</button></div>'}).join("")+'</div><div class="cl-more" id="clm" hidden><span>'+(D.examples.length>3?(D.examples.length-3)+" more prompts":"More below")+'</span></div></div>'
|
|
473
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div><button type="button" class="cl-go" style="margin-top:14px" data-a="next2">'+(k?"Continue with "+k+" prompt"+(k>1?"s":""):"Continue without prompts")+'</button>'
|
|
474
|
+
+(k?'<button type="button" class="cl-skip" data-a="skip2">Skip, don’t share any prompts</button>':'')}
|
|
475
|
+
else if(n===3){var p=payload(),hh=esc(CS.handle);H=prog(3)+'<h2 class="cl-h">Make it work for you.</h2><p class="cl-p">All optional. Change any of it later from your profile.</p>'
|
|
476
|
+
+'<label class="cl-opt dk"><span class="cl-oh"><span>Make <span class="grad">@'+hh+'</span> public</span><input type="checkbox" data-o="pub"'+(CS.pub?" checked":"")+'></span><span class="cl-ot">A link for your LinkedIn, GitHub and email signature. It shows how you work, which a CV can’t.</span><span class="cl-ot s">Off: your handle stays reserved, and only people with your private link can see the profile.</span></label>'
|
|
477
|
+
+'<label class="cl-opt"><span class="cl-oh"><span>Let companies come to you</span><input type="checkbox" data-o="roles"'+(CS.roles?" checked":"")+'></span><ul class="cl-ul" style="--dot:#2B5FE3"><li>Teams hiring AI builders see your profile and ask for an intro. No applying, no cover letters.</li><li>You say yes or no to each one. Your email stays hidden until you say yes.</li></ul></label>'
|
|
478
|
+
+'<label class="cl-opt"><span class="cl-oh"><span>Get featured by Hour</span><input type="checkbox" data-o="feature"'+(CS.feature?" checked":"")+'></span><ul class="cl-ul" style="--dot:#F2601F"><li>We post standout cards in our “builders to watch” series, tagged to you.</li><li>Your work in front of founders and hiring teams who follow Hour.</li></ul></label>'
|
|
479
|
+
+'<h3 class="cl-h3">What goes to Hour</h3><div class="cl-two"><div><b><i style="background:#2B5FE3"></i>Sent</b><span>Your card numbers</span><span>'+(p.prompts.length?p.prompts.length+" prompt"+(p.prompts.length>1?"s":"")+" you picked":"No prompts")+'</span><span>Handle, name, email</span></div><div><b><i style="background:#F2601F"></i>Stays with you</b><span>Your logs and code</span><span>Every other prompt</span><span>File and project names</span></div></div>'
|
|
480
|
+
+'<button type="button" class="cl-link" data-a="data">'+(CS.data?"Hide the exact data":"See the exact data")+'</button>'+(CS.data?'<pre>'+esc(JSON.stringify(p,null,2))+'</pre>':'')
|
|
481
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div><button type="button" class="cl-go" data-a="create"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Creating…":"Create @"+hh)+'</button><p class="cl-fine c">Delete it any time, with one link.</p>'}
|
|
482
|
+
else{var d=CS.done,u=d.publicUrl||d.url;H='<div class="cl-top"><span></span><button type="button" class="cl-back" data-a="close">Close</button></div><div class="cl-done"><h2>@'+esc(d.handle)+'<br>is yours.</h2><a class="u" href="'+esc(u)+'" target="_blank" rel="noopener">'+esc(u.replace("https://",""))+'</a><p>'+(d.publicUrl?"Your profile is public. Put the link anywhere you like.":"Your profile is private. Only people you send this link to can see it.")+'</p></div>'
|
|
483
|
+
+'<div class="cl-row"><a href="'+esc(u)+'" target="_blank" rel="noopener">Open my profile</a><button type="button" data-a="copy">Copy link</button></div><p class="cl-fine c">We emailed you this link, plus a private link to change your settings or delete your profile.</p>'}
|
|
484
|
+
s.innerHTML=H;s.scrollTop=0;
|
|
485
|
+
if(n===2){var L=document.getElementById("cll"),M=document.getElementById("clm"),f=function(){M.hidden=L.scrollHeight-L.clientHeight-L.scrollTop<24};L.addEventListener("scroll",f);f();var ta=s.querySelector("textarea");if(ta)ta.focus()}}
|
|
486
|
+
document.getElementById("cls").addEventListener("input",function(e){var t=e.target;
|
|
487
|
+
if(t.id==="clh"){var v=t.value.toLowerCase().replace(/[^a-z0-9-]/g,"");if(v!==t.value)t.value=v;CS.handle=v;CS.err="";document.getElementById("clhst").textContent=handleNote()}
|
|
488
|
+
else if(t.id==="cln")CS.name=t.value;else if(t.id==="cle")CS.email=t.value;
|
|
489
|
+
else if(t.tagName==="TEXTAREA")CS.texts[+t.getAttribute("data-i")]=t.value});
|
|
490
|
+
document.getElementById("cls").addEventListener("change",function(e){var o=e.target.getAttribute("data-o");if(o){CS[o]=e.target.checked;if(CS.data)renderClaim()}});
|
|
491
|
+
document.getElementById("cls").addEventListener("click",function(e){var b=e.target.closest("[data-a]");if(!b||e.target.tagName==="TEXTAREA")return;var a=b.getAttribute("data-a"),i=+b.getAttribute("data-i");
|
|
492
|
+
if(a==="back"){CS.err="";if(CS.step===1)closeClaim();else{CS.step=CS.step===3&&!D.examples.length?1:CS.step-1;renderClaim()}}
|
|
493
|
+
else if(a==="close"){closeClaim()}
|
|
494
|
+
else if(a==="next1"){if(CS.busy)return;if(!HANDLE_RE.test(CS.handle)){CS.err="Handles are 3 to 24 letters, numbers or single dashes.";return renderClaim()}if(!CS.name.trim()){CS.err="Add your name.";return renderClaim()}if(!EMAIL_RE.test(CS.email.trim())){CS.err="Add an email address, so the handle stays yours.";return renderClaim()}
|
|
495
|
+
CS.busy=true;CS.err="";renderClaim();fetch(D.api+"/handles/"+encodeURIComponent(CS.handle)).then(function(r){return r.json()}).then(function(j){CS.busy=false;if(j.available){CS.step=D.examples.length?2:3}else CS.err="@"+CS.handle+(j.reason==="already taken"?" is taken. Try another.":": "+j.reason+".");renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Check your connection and try again.";renderClaim()})}
|
|
496
|
+
else if(a==="edit"){e.stopPropagation();CS.editing=CS.editing===i?-1:i;renderClaim()}
|
|
497
|
+
else if(a==="pick"){var at=CS.picked.indexOf(i);CS.err="";if(at>=0)CS.picked.splice(at,1);else if(CS.picked.length>=3)CS.err="Pick up to 3. Untick one first.";else CS.picked.push(i);var y=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y}
|
|
498
|
+
else if(a==="next2"){CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
499
|
+
else if(a==="skip2"){CS.picked=[];CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
500
|
+
else if(a==="data"){CS.data=!CS.data;renderClaim()}
|
|
501
|
+
else if(a==="copy"){var u=CS.done.publicUrl||CS.done.url;(navigator.clipboard?navigator.clipboard.writeText(u):Promise.reject()).then(function(){b.textContent="Copied"},function(){window.prompt("Copy this link",u)})}
|
|
502
|
+
else if(a==="create"){if(CS.busy)return;CS.busy=true;CS.err="";renderClaim();
|
|
503
|
+
fetch(D.api+"/profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload())}).then(function(r){return r.json().catch(function(){return{}}).then(function(j){return{s:r.status,j:j}})}).then(function(x){CS.busy=false;
|
|
504
|
+
if(x.s===201&&x.j.url){CS.done=x.j;CS.step=4;document.querySelector(".top .note").textContent="Your profile is on usehour.com. Your logs and other prompts stayed on this computer.";document.getElementById("claimbar").hidden=true}
|
|
505
|
+
else if(x.s===409){CS.step=1;CS.err="Someone just took @"+CS.handle+". Pick another."}
|
|
506
|
+
else CS.err="That didn’t go through ("+(x.j.error||"error "+x.s)+"). Nothing was saved. Try again in a minute.";renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Nothing was saved. Try again.";renderClaim()})}});
|
|
507
|
+
document.getElementById("claimb").onclick=openClaim;
|
|
508
|
+
`;
|
|
509
|
+
|
|
281
510
|
/* --------------------------------------------------------------- the page */
|
|
282
|
-
function pageHTML(m, C) {
|
|
283
|
-
const
|
|
511
|
+
function pageHTML(m, C, o = {}) {
|
|
512
|
+
const api = o.api || null, examples = api ? o.examples || [] : [];
|
|
513
|
+
const data = JSON.stringify({ hours: m.hours, months: m.months.map((x) => x.prompts), cards: C, m: api ? m : null, examples, api, suggest: api ? o.suggest || "" : "" }).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
514
|
+
/* The page may talk to exactly one place: Hour's profile endpoint, and only
|
|
515
|
+
when you press the button. With --no-share it may talk to nothing. */
|
|
516
|
+
const connect = api ? new URL(api).origin : "'none'";
|
|
517
|
+
const csp = `default-src 'none'; img-src data: blob:; font-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src ${connect}; form-action 'none'; base-uri 'none'`;
|
|
284
518
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
519
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
285
520
|
<title>Your AI Wrapped · Hour</title>
|
|
286
521
|
<style>
|
|
287
522
|
@font-face{font-family:"Geist";src:url(data:font/woff2;base64,${FONT_GEIST}) format("woff2");font-weight:100 900}
|
|
@@ -307,6 +542,8 @@ h2{font-size:24px;letter-spacing:-.03em;margin:0 0 14px}
|
|
|
307
542
|
.ft{display:flex;justify-content:space-between;align-items:center;margin-top:6cqw;font-size:3.4cqw;opacity:.8}.ft .b{display:flex;align-items:center;gap:1.6cqw;font-weight:600}.ft svg{width:4.6cqw;height:4.6cqw}.ft .u{font-family:var(--mono)}
|
|
308
543
|
.sum{margin-top:auto;display:grid;grid-template-columns:1fr 1fr;gap:4.5cqw 5cqw}.sum div{border-top:.35cqw solid currentColor;padding-top:2cqw}.sum small{display:block;font-size:3.4cqw;opacity:.75;margin-bottom:.6cqw}.sum b{font-size:6.2cqw;letter-spacing:-.035em;font-weight:650;line-height:1}
|
|
309
544
|
.arch{font-size:11.5cqw;line-height:.92;letter-spacing:-.05em;font-weight:650;margin:2cqw 0 6cqw}
|
|
545
|
+
.rows{margin-top:7cqw;display:grid;gap:3.4cqw}.rows div{display:grid;grid-template-columns:1fr auto 10cqw;align-items:center;gap:3cqw;font-size:4.3cqw;font-weight:500}.rows em{display:flex;gap:1.1cqw}.rows u{width:2.7cqw;height:2.7cqw;border-radius:50%;background:currentColor;opacity:.14}.rows u.on{opacity:1;background:#8B5FD9}.rows b{text-align:right;font-weight:600}
|
|
546
|
+
${api ? CLAIM_CSS : ""}
|
|
310
547
|
.grad{background:linear-gradient(100deg,#4F88FF 0%,#B0ACFE 38%,#FB65C2 72%,#FD2980 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
311
548
|
</style></head><body>
|
|
312
549
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><linearGradient id="ob" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#4F88FF"/><stop offset="38%" stop-color="#B0ACFE"/><stop offset="72%" stop-color="#FB65C2"/><stop offset="100%" stop-color="#FD2980"/></linearGradient><symbol id="mark" viewBox="0 0 64 64"><path fill="url(#ob)" d="${MARK_PATH}"/></symbol></defs></svg>
|
|
@@ -315,14 +552,15 @@ h2{font-size:24px;letter-spacing:-.03em;margin:0 0 14px}
|
|
|
315
552
|
<div class="stage" id="stage"><div class="bars" id="bars"></div><div id="slot"></div><button class="hit l" id="prev" aria-label="Previous"></button><button class="hit r" id="next" aria-label="Next"></button></div>
|
|
316
553
|
<div class="nav"><button id="b1">Back</button><button id="b2">Next</button></div>
|
|
317
554
|
<h2>All cards</h2><div class="grid" id="grid"></div>
|
|
318
|
-
<p class="note" style="margin-top:24px">To post a card, click "Save image" under it. The numbers are in wrapped.json next to this file
|
|
555
|
+
<p class="note" style="margin-top:24px">To post a card, click "Save image" under it. The numbers are in wrapped.json next to this file.</p>
|
|
556
|
+
${api ? CLAIM_HTML : ""}
|
|
319
557
|
</div>
|
|
320
558
|
<script>
|
|
321
559
|
const D=${data};
|
|
322
560
|
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
|
323
561
|
function el(c){const d=document.createElement("article");d.className="card";d.style.setProperty("--bg",c.bg);d.style.setProperty("--fg",c.fg);if(c.bs)d.style.setProperty("--bs",c.bs);
|
|
324
562
|
const ft='<div class="ft"><span class="b"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</span><span class="u">usehour.com</span></div>';
|
|
325
|
-
d.innerHTML='<canvas aria-hidden="true"></canvas><div class="in">'+(c.summary?('<div class="ask">'+esc(c.who?c.who+" \\u00B7 ":"")+esc(c.period)+'</div><div class="arch grad">'+esc(c.type)+'</div><div class="sum">'+c.stats.map(s=>'<div><small>'+esc(s[0])+'</small><b>'+esc(s[1])+'</b></div>').join("")+'</div>'):('<div class="ask">'+esc(c.ask)+'</div><div class="big'+(c.grad?" grad":"")+'">'+esc(c.big)+'</div><div class="line">'+esc(c.line)+'</div>'))+ft+'</div>';
|
|
563
|
+
d.innerHTML='<canvas aria-hidden="true"></canvas><div class="in">'+(c.summary?('<div class="ask">'+esc(c.who?c.who+" \\u00B7 ":"")+esc(c.period)+'</div><div class="arch grad">'+esc(c.type)+'</div><div class="sum">'+c.stats.map(s=>'<div><small>'+esc(s[0])+'</small><b>'+esc(s[1])+'</b></div>').join("")+'</div>'):('<div class="ask">'+esc(c.ask)+'</div>'+(c.rows?'<div class="rows">'+c.rows.map(w=>'<div><span>'+esc(w[0])+'</span><em>'+Array.from({length:10},(_,k)=>'<u'+(k<Math.round(w[1]*10)?' class="on"':'')+'></u>').join("")+'</em><b>'+esc(w[2])+'</b></div>').join("")+'</div>':'')+'<div class="big'+(c.grad?" grad":"")+'">'+esc(c.big)+'</div><div class="line">'+esc(c.line)+'</div>'))+ft+'</div>';
|
|
326
564
|
requestAnimationFrame(()=>draw(d,c));return d}
|
|
327
565
|
function draw(card,c){const cv=card.querySelector("canvas"),r=card.getBoundingClientRect(),dpr=Math.min(devicePixelRatio||1,2),W=Math.max(1,r.width),H=Math.max(1,r.height);cv.width=W*dpr;cv.height=H*dpr;const g=cv.getContext("2d");g.scale(dpr,dpr);drawOn(g,W,H,c)}
|
|
328
566
|
function drawOn(x,W,H,c){if(!c.pat)return;
|
|
@@ -349,13 +587,15 @@ c.stats.forEach((s,k)=>{const x=P+(k%2)*(cw+W*.05),yy=top+Math.floor(k/2)*rowH;g
|
|
|
349
587
|
g.font="650 "+W*.115+"px "+F;g.fillStyle=grad();g.fillText(c.type,P,W*.33);g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;g.fillText((c.who?c.who+" · ":"")+c.period,P,W*.16);g.globalAlpha=1;return}
|
|
350
588
|
g.font="450 "+W*.056+"px "+F;const L=wrapText(g,c.line,(W-2*P)*.92);L.reverse().forEach((l,i)=>g.fillText(l,P,y-i*W*.07));y-=L.length*W*.07+W*.034;
|
|
351
589
|
let bs=parseFloat(c.bs||"19")/100*W;g.font="650 "+bs+"px "+F;let Bl=wrapText(g,c.big,W-2*P);while(Bl.length>1&&Bl[Bl.length-1].length<=2&&bs>W*.08){bs*=.92;g.font="650 "+bs+"px "+F;Bl=wrapText(g,c.big,W-2*P)}g.fillStyle=c.grad?grad():c.fg;Bl.reverse().forEach((l,i)=>g.fillText(l,P,y-i*bs*.9));
|
|
352
|
-
g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;wrapText(g,c.ask,(W-2*P)*.62).forEach((l,i)=>g.fillText(l,P,W*.16+i*W*.055));g.globalAlpha=1
|
|
590
|
+
g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;wrapText(g,c.ask,(W-2*P)*.62).forEach((l,i)=>g.fillText(l,P,W*.16+i*W*.055));g.globalAlpha=1;
|
|
591
|
+
if(c.rows){const d=W*.027,gap=W*.011,pw=W*.1,dx=W-P-pw-10*d-9*gap;c.rows.forEach((w,k)=>{const yy=W*.3+k*W*.1;g.fillStyle=c.fg;g.font="500 "+W*.043+"px "+F;g.fillText(w[0],P,yy);g.font="600 "+W*.043+"px "+F;g.textAlign="right";g.fillText(w[2],W-P,yy);g.textAlign="left";for(let q=0;q<10;q++){const on=q<Math.round(w[1]*10);g.globalAlpha=on?1:.14;g.fillStyle=on?"#8B5FD9":c.fg;g.beginPath();g.arc(dx+q*(d+gap)+d/2,yy-W*.015,d/2,0,7);g.fill()}g.globalAlpha=1})}}
|
|
353
592
|
async function save(c){try{await document.fonts.ready}catch(e){}const cv=document.createElement("canvas");cv.width=1080;cv.height=1350;paint(cv.getContext("2d"),1080,1350,c);const a=document.createElement("a");a.download="hour-"+c.id+".png";a.href=cv.toDataURL("image/png");document.body.appendChild(a);a.click();a.remove()}
|
|
354
593
|
let cur=0;const slot=document.getElementById("slot"),bars=document.getElementById("bars"),grid=document.getElementById("grid");
|
|
355
|
-
D.cards.
|
|
356
|
-
function show(n){cur=(n+
|
|
594
|
+
const N=D.cards.length+(D.api?1:0);for(let k=0;k<N;k++)bars.appendChild(document.createElement("i"));
|
|
595
|
+
function show(n){cur=(n+N)%N;slot.innerHTML="";const cl=cur>=D.cards.length;prev.style.display=next.style.display=cl?"none":"";slot.appendChild(cl?claimEl():el(D.cards[cur]));[...bars.children].forEach((b,k)=>b.classList.toggle("on",k<=cur))}
|
|
357
596
|
D.cards.forEach((c,k)=>{const w=document.createElement("div");const e=el(c);e.onclick=()=>show(k);const s=document.createElement("a");s.href="#";s.className="save";s.textContent="Save image";s.onclick=ev=>{ev.preventDefault();save(c)};w.append(e,s);grid.appendChild(w)});
|
|
358
|
-
prev.onclick=b1.onclick=()=>show(cur-1);next.onclick=b2.onclick=()=>show(cur+1);addEventListener("keydown",e=>{if(e.key==="ArrowRight")show(cur+1);if(e.key==="ArrowLeft")show(cur-1)});show(0);
|
|
597
|
+
prev.onclick=b1.onclick=()=>show(cur-1);next.onclick=b2.onclick=()=>show(cur+1);addEventListener("keydown",e=>{const o=document.getElementById("cl");if(o&&!o.hidden)return;if(e.key==="ArrowRight")show(cur+1);if(e.key==="ArrowLeft")show(cur-1)});show(0);
|
|
598
|
+
${api ? CLAIM_JS : ""}
|
|
359
599
|
</script></body></html>`;
|
|
360
600
|
}
|
|
361
601
|
|
|
@@ -380,12 +620,10 @@ function openFile(f) {
|
|
|
380
620
|
const args = process.platform === "win32" ? ["/c", "start", "", f] : [f];
|
|
381
621
|
try { spawn(cmd, args, { stdio: "ignore", detached: true }).unref(); } catch { /* the path is printed anyway */ }
|
|
382
622
|
}
|
|
383
|
-
/* Sharing stays off until usehour.com can store profiles. */
|
|
384
|
-
const SHARE_OPEN = false;
|
|
385
623
|
async function main() {
|
|
386
624
|
const argv = process.argv.slice(2), flag = (n) => argv.includes(n), opt = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : null; };
|
|
387
625
|
if (flag("--help") || flag("-h")) {
|
|
388
|
-
console.log(`Hour Wrapped ${VERSION}\n\n npx usehour [--yes] [--no-open] [--out DIR] [--claude-dir DIR] [--codex-dir DIR]
|
|
626
|
+
console.log(`Hour Wrapped ${VERSION}\n\n npx usehour [--yes] [--no-open] [--no-share] [--out DIR] [--claude-dir DIR] [--codex-dir DIR]\n\n --yes skip the first question (reading still happens only on this machine)\n --no-open don't open the browser\n --no-share cards only: no example prompts, and the cards page cannot connect anywhere\n --out DIR where to write hour-wrapped/ (default: current folder)\n`);
|
|
389
627
|
return;
|
|
390
628
|
}
|
|
391
629
|
const home = os.homedir();
|
|
@@ -394,6 +632,7 @@ async function main() {
|
|
|
394
632
|
{ src: "codex", label: "Codex", dir: opt("--codex-dir") || path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "sessions") },
|
|
395
633
|
].map((s) => ({ ...s, files: walk(s.dir) }));
|
|
396
634
|
const found = sources.filter((s) => s.files.length);
|
|
635
|
+
const share = !flag("--no-share");
|
|
397
636
|
console.log(`\n ${B("Hour Wrapped")} ${DIM("v" + VERSION)} Your year of building with AI.\n`);
|
|
398
637
|
if (!found.length) {
|
|
399
638
|
console.log(` No Claude Code or Codex sessions found.\n Looked in:\n${sources.map((s) => " " + s.dir).join("\n")}\n\n If yours live somewhere else, run with --claude-dir or --codex-dir.\n`);
|
|
@@ -402,8 +641,8 @@ async function main() {
|
|
|
402
641
|
const bytes = found.reduce((a, s) => a + s.files.reduce((b, f) => { try { return b + fs.statSync(f).size; } catch { return b; } }, 0), 0);
|
|
403
642
|
console.log(" This reads your session logs, on this machine only:");
|
|
404
643
|
for (const s of found) console.log(` ${s.label.padEnd(12)} ${s.files.length} files ${DIM(s.dir)}`);
|
|
405
|
-
console.log(`\n It keeps counts
|
|
406
|
-
console.log(`
|
|
644
|
+
console.log(`\n It keeps counts${share ? ", plus a few of your best prompts for you to choose from" : ""}.`);
|
|
645
|
+
console.log(` This script never connects to the internet.\n`);
|
|
407
646
|
if (!flag("--yes")) {
|
|
408
647
|
if (!process.stdin.isTTY) { console.log(" Run this in a terminal (or add --yes)."); process.exitCode = 1; return; }
|
|
409
648
|
const a = await ask(` Read ${(bytes / 1e9).toFixed(1)} GB of logs and make your cards? [y/N] `);
|
|
@@ -421,7 +660,7 @@ async function main() {
|
|
|
421
660
|
if (next >= jobs.length) { w.terminate(); if (--active === 0) resolve(); return; }
|
|
422
661
|
const job = jobs[next++];
|
|
423
662
|
w.once("message", (r) => { if (r.ok) summaries.push(r.s); done++; doneBytes += job.size; tick(); feed(w); });
|
|
424
|
-
w.postMessage({ file: job.file, src: job.src });
|
|
663
|
+
w.postMessage({ file: job.file, src: job.src, examples: share });
|
|
425
664
|
};
|
|
426
665
|
workers.forEach((w) => { active++; feed(w); });
|
|
427
666
|
});
|
|
@@ -434,35 +673,18 @@ async function main() {
|
|
|
434
673
|
fs.mkdirSync(outDir, { recursive: true });
|
|
435
674
|
fs.writeFileSync(path.join(outDir, "wrapped.json"), JSON.stringify(m, null, 2));
|
|
436
675
|
const page = path.join(outDir, "index.html");
|
|
437
|
-
|
|
676
|
+
const examples = share ? pickExamples(summaries, 10) : [];
|
|
677
|
+
const suggest = (() => { try { const u = os.userInfo().username.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "").slice(0, 24); return ["root", "admin", "user", "ubuntu", "runner", "administrator", "guest", "owner"].includes(u) ? "" : u; } catch { return ""; } })();
|
|
678
|
+
fs.writeFileSync(page, pageHTML(m, C, { api: share ? (opt("--api") || API_DEFAULT).replace(/\/+$/, "") : null, examples, suggest: suggest.length >= 3 ? suggest : "" }));
|
|
438
679
|
const type = TYPES[builderType(m)].name;
|
|
439
680
|
console.log(`\n ${B("You're " + type + ".")} ${m.sessions.total} sessions, ${m.prompts.total.toLocaleString("en")} prompts, busiest at ${hourLabel(m.busiestHour)}.`);
|
|
440
681
|
console.log(` Your cards: ${page}\n`);
|
|
441
682
|
if (!flag("--no-open")) openFile(page);
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!yes(a1)) { console.log("\n Done. Nothing was sent.\n"); return; }
|
|
448
|
-
const email = await ask(" Your email (so we can send you the link): ");
|
|
449
|
-
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { console.log("\n That doesn't look like an email. Nothing was sent.\n"); return; }
|
|
450
|
-
const name = await ask(" Your name, as it should appear on the page: ");
|
|
451
|
-
const linkedin = await ask(` LinkedIn URL ${DIM("(optional, press Enter to skip)")}: `);
|
|
452
|
-
const pub = yes(await ask(" Make your page public so you can share the link? [y/N] "));
|
|
453
|
-
const feature = yes(await ask(" OK for Hour to feature your cards in posts about great AI builders? [y/N] "));
|
|
454
|
-
const roles = yes(await ask(" Open to hearing about roles where this matters? [y/N] "));
|
|
455
|
-
const payload = { email, name, linkedin, public: pub, feature, roles, wrapped: m };
|
|
456
|
-
console.log(`\n This is exactly what will be sent to ${opt("--share-url") || SHARE_URL_DEFAULT}:\n`);
|
|
457
|
-
console.log(JSON.stringify(payload, null, 2).split("\n").map((l) => " " + l).join("\n"));
|
|
458
|
-
if (!yes(await ask("\n Send it? [y/N] "))) { console.log("\n Not sent. Your cards are still on your machine.\n"); return; }
|
|
459
|
-
const body = new URLSearchParams({ "form-name": "wrapped", email, name, linkedin, public: String(pub), feature: String(feature), roles: String(roles), data: JSON.stringify(m) });
|
|
460
|
-
try {
|
|
461
|
-
const r = await fetch(opt("--share-url") || SHARE_URL_DEFAULT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, redirect: "manual" });
|
|
462
|
-
if (r.status >= 200 && r.status < 400) console.log(`\n Sent. We'll email ${email} your link within a day.\n`);
|
|
463
|
-
else console.log(`\n The upload didn't go through (${r.status}). Email wrapped.json to lush@usehour.com instead.\n`);
|
|
464
|
-
} catch (e) {
|
|
465
|
-
console.log(`\n Couldn't reach usehour.com (${e.message}). Email wrapped.json to lush@usehour.com instead.\n`);
|
|
683
|
+
if (share) {
|
|
684
|
+
console.log(` Want a profile? The last card lets you claim your handle at usehour.com/@you.`);
|
|
685
|
+
console.log(` Nothing is sent unless you press the buttons there. You see exactly what goes first.`);
|
|
686
|
+
if (examples.length) console.log(DIM(` (The cards page holds ${examples.length} of your prompts so you can choose. It stays on this computer.)`));
|
|
687
|
+
console.log("");
|
|
466
688
|
}
|
|
467
689
|
}
|
|
468
690
|
/* ------------------------------------------------------------- assets
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usehour",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Your year of building with AI, as Wrapped-style cards. Reads your Claude Code and Codex logs locally.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Your year of building with AI, as Wrapped-style cards, and an optional private profile. Reads your Claude Code and Codex logs locally.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "usehour": "hour.mjs" },
|
|
7
7
|
"files": ["hour.mjs"],
|