usehour 0.4.0 → 0.4.1
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 +2 -2
- package/hour.mjs +80 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Your year of building with AI, as Wrapped-style cards.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
npx usehour@0.4.
|
|
6
|
+
npx usehour@0.4.1
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
- Asks before it reads anything, and lists the folders it will read.
|
|
@@ -11,7 +11,7 @@ npx usehour@0.4.0
|
|
|
11
11
|
- Counts your habits, and finds a few of your best prompts (anything that looks like a key, password, email or home folder already hidden). You can pick some for your profile and black out words, but not add or change any.
|
|
12
12
|
- Opens your cards in your browser. Save any card as an image to post it.
|
|
13
13
|
- Optional: claim usehour.com/@you from the cards page. You see exactly what is sent first. The script itself never connects to the internet.
|
|
14
|
-
- Counts only sessions you started yourself: sub-agents and runs started by scripts or SDKs are left out. `npx usehour@0.4.
|
|
14
|
+
- Counts only sessions you started yourself: sub-agents and runs started by scripts or SDKs are left out. `npx usehour@0.4.1 --explain` shows what was counted and what was left out, in your terminal only.
|
|
15
15
|
- No dependencies, nothing installed.
|
|
16
16
|
|
|
17
17
|
It's one file: read [hour.mjs](https://usehour.com/hour.mjs) before you run it.
|
package/hour.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { spawn } from "node:child_process";
|
|
|
31
31
|
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
|
|
34
|
-
const VERSION = "0.4.
|
|
34
|
+
const VERSION = "0.4.1";
|
|
35
35
|
const API_DEFAULT = "https://usehour.com/api";
|
|
36
36
|
|
|
37
37
|
/* ------------------------------------------------------------------ patterns */
|
|
@@ -1017,7 +1017,36 @@ function explain(summaries) {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
1019
|
/* ------------------------------------------------------------------ the run */
|
|
1020
|
-
|
|
1020
|
+
/* Terminal look. Colour only on a real terminal, never with NO_COLOR or
|
|
1021
|
+
TERM=dumb. 24-bit colour where the terminal says it has it, otherwise the
|
|
1022
|
+
nearest of the 256 standard colours. No packages: plain escape codes. */
|
|
1023
|
+
const COLOR = !!process.stdout.isTTY && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb";
|
|
1024
|
+
const TRUECOLOR = /truecolor|24bit/i.test(process.env.COLORTERM || "") || /iTerm|WezTerm|vscode|ghostty|Warp/i.test(process.env.TERM_PROGRAM || "");
|
|
1025
|
+
const E = (c) => (COLOR ? `\x1b[${c}m` : "");
|
|
1026
|
+
const B = (s) => E(1) + s + E(0), DIM = (s) => E(2) + s + E(0);
|
|
1027
|
+
const c256 = ([r, g, b]) => { const q = (v) => (v < 48 ? 0 : v < 115 ? 1 : Math.min(5, Math.round((v - 35) / 40))); return 16 + 36 * q(r) + 6 * q(g) + q(b); };
|
|
1028
|
+
const FG = (rgb) => E(TRUECOLOR ? `38;2;${rgb.join(";")}` : `38;5;${c256(rgb)}`), BG = (rgb) => E(TRUECOLOR ? `48;2;${rgb.join(";")}` : `48;5;${c256(rgb)}`);
|
|
1029
|
+
const HUE = { lav: [176, 172, 254], green: [95, 211, 141], blue: [79, 136, 255], purple: [139, 95, 217], orange: [242, 96, 31], mustard: [219, 160, 4], pink: [251, 101, 194] };
|
|
1030
|
+
const tint = (h, s) => FG(HUE[h]) + s + E(0);
|
|
1031
|
+
const GSTOP = [[0, [79, 136, 255]], [0.38, [176, 172, 254]], [0.72, [251, 101, 194]], [1, [253, 41, 128]]];
|
|
1032
|
+
const gcol = (t) => { t = Math.max(0, Math.min(1, t)); let i = 0; while (i < GSTOP.length - 2 && t > GSTOP[i + 1][0]) i++; const [a, x] = GSTOP[i], [b, y] = GSTOP[i + 1], f = (t - a) / (b - a); return x.map((v, k) => Math.round(v + (y[k] - v) * f)); };
|
|
1033
|
+
const grad = (s, bold) => (COLOR ? [...s].map((ch, i, a) => (ch === " " ? " " : FG(gcol(i / Math.max(1, a.length - 1))) + (bold ? E(1) : "") + ch)).join("") + E(0) : s);
|
|
1034
|
+
const KEY = (s) => (COLOR ? E(7) + " " + s + " " + E(27) : "[" + s + "]");
|
|
1035
|
+
/* The Hour ring as a dot matrix, like the cards: bright dots in the logo's
|
|
1036
|
+
gradient on a faint grid. Plain characters, so every terminal shows it. */
|
|
1037
|
+
function markLines(n = 7, R = 2.8) {
|
|
1038
|
+
const c0 = (n - 1) / 2, out = [];
|
|
1039
|
+
for (let r = 0; r < n; r++) {
|
|
1040
|
+
let l = "";
|
|
1041
|
+
for (let c = 0; c < n; c++) {
|
|
1042
|
+
const d = Math.hypot(c - c0, r - c0);
|
|
1043
|
+
l += (Math.abs(d - R) < 0.5 ? FG(gcol((c + r) / (2 * (n - 1)))) + "●" + E(0) : d < R + 1.1 ? DIM("·") : " ") + (c < n - 1 ? " " : "");
|
|
1044
|
+
}
|
|
1045
|
+
out.push(l);
|
|
1046
|
+
}
|
|
1047
|
+
return out;
|
|
1048
|
+
}
|
|
1049
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1021
1050
|
/* One reader for the whole run, so answers typed ahead are never lost. */
|
|
1022
1051
|
let RL = null, LINES = [], WAIT = null;
|
|
1023
1052
|
function ask(q) {
|
|
@@ -1031,7 +1060,7 @@ function ask(q) {
|
|
|
1031
1060
|
return new Promise((res) => { WAIT = res; });
|
|
1032
1061
|
}
|
|
1033
1062
|
function done() { if (RL) RL.close(); }
|
|
1034
|
-
const yes = (a) =>
|
|
1063
|
+
const yes = (a) => !/^n(o)?$/i.test(a); // Enter means yes: running the command was the choice
|
|
1035
1064
|
function openFile(f) {
|
|
1036
1065
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1037
1066
|
const args = process.platform === "win32" ? ["/c", "start", "", f] : [f];
|
|
@@ -1050,25 +1079,45 @@ async function main() {
|
|
|
1050
1079
|
].map((s) => ({ ...s, files: walk(s.dir) }));
|
|
1051
1080
|
const found = sources.filter((s) => s.files.length);
|
|
1052
1081
|
const share = !flag("--no-share");
|
|
1053
|
-
|
|
1082
|
+
if (COLOR) {
|
|
1083
|
+
const mk = markLines(), mid = Math.floor(mk.length / 2);
|
|
1084
|
+
console.log("");
|
|
1085
|
+
mk.forEach((l, i) => console.log(" " + l + " " + (i === mid - 1 ? `${B("Hour Wrapped")} ${DIM("v" + VERSION)}` : i === mid ? "Your year of building with AI." : "")));
|
|
1086
|
+
console.log("");
|
|
1087
|
+
} else console.log(`\n Hour Wrapped v${VERSION} Your year of building with AI.\n`);
|
|
1054
1088
|
if (!found.length) {
|
|
1055
1089
|
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`);
|
|
1056
1090
|
process.exitCode = 1; return;
|
|
1057
1091
|
}
|
|
1058
1092
|
const bytes = found.reduce((a, s) => a + s.files.reduce((b, f) => { try { return b + fs.statSync(f).size; } catch { return b; } }, 0), 0);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
console.log(
|
|
1062
|
-
console.log(`
|
|
1063
|
-
console.log(
|
|
1093
|
+
const here = process.platform === "darwin" ? "this Mac" : "this computer";
|
|
1094
|
+
const tick = (t) => " " + tint("green", "✓") + " " + t;
|
|
1095
|
+
console.log(` ${tint("lav", "●")} ${B("Offline.")} ${tint("lav", "Nothing leaves this computer.")}`);
|
|
1096
|
+
console.log(tick(`Reads your logs here, on ${here}`));
|
|
1097
|
+
console.log(tick("Keeps counts and labels only. No prompts, code or files are sent"));
|
|
1098
|
+
console.log(tick(`No internet needed. ${DIM("Try it with Wi-Fi off.")}`));
|
|
1099
|
+
console.log(`\n ${DIM("Found")}`);
|
|
1100
|
+
const tilde = (p) => (p.startsWith(home) ? "~" + p.slice(home.length) : p);
|
|
1101
|
+
const wFiles = Math.max(...found.map((s) => String(s.files.length).length));
|
|
1102
|
+
for (const s of found) console.log(` ${s.label.padEnd(13)} ${B(String(s.files.length).padStart(wFiles))} ${s.files.length === 1 ? "file " : "files"} ${DIM(tilde(s.dir))}`);
|
|
1103
|
+
console.log("");
|
|
1064
1104
|
if (!flag("--yes")) {
|
|
1065
1105
|
if (!process.stdin.isTTY) { console.log(" Run this in a terminal (or add --yes)."); process.exitCode = 1; return; }
|
|
1066
|
-
const
|
|
1106
|
+
const size = bytes >= 1e9 ? (bytes / 1e9).toFixed(1) + " GB" : Math.max(1, Math.round(bytes / 1e6)) + " MB";
|
|
1107
|
+
const a = await ask(` Read ${B(size)} of logs and make your cards? ${COLOR ? `${KEY("Enter")} yes ${DIM("·")} ${KEY("n")} no` : "[Y/n]"} `);
|
|
1067
1108
|
if (!yes(a)) { console.log("\n Stopped. Nothing was read.\n"); return; }
|
|
1068
1109
|
}
|
|
1069
1110
|
const jobs = found.flatMap((s) => s.files.map((file) => ({ file, src: s.src, size: fs.statSync(file).size }))).sort((a, b) => b.size - a.size);
|
|
1070
1111
|
const summaries = []; let done = 0, doneBytes = 0; const t0 = Date.now();
|
|
1071
|
-
const
|
|
1112
|
+
const TTY = !!process.stdout.isTTY, BAR = Math.max(10, Math.min(24, (process.stdout.columns || 80) - 50)), nf = (n) => n.toLocaleString("en");
|
|
1113
|
+
let found_p = 0, last = 0;
|
|
1114
|
+
const bar = (p) => { const k = Math.round(p * BAR); let o = ""; for (let i = 0; i < BAR; i++) o += i < k ? (COLOR ? FG(gcol(i / (BAR - 1))) + "●" : "●") : (COLOR ? E(2) + "·" : "·"); return o + E(0); };
|
|
1115
|
+
const progress = (force) => {
|
|
1116
|
+
if (!TTY) return; const now = Date.now(); if (!force && now - last < 60) return; last = now;
|
|
1117
|
+
const p = doneBytes / Math.max(1, bytes);
|
|
1118
|
+
process.stdout.write(`\r\x1b[K ${bar(p)} ${B(String(Math.round(p * 100)).padStart(3) + "%")} ${DIM(`${nf(done)}/${nf(jobs.length)} files ·`)} ${B(nf(found_p))} ${DIM("prompts")}`);
|
|
1119
|
+
};
|
|
1120
|
+
if (TTY) console.log(`\n Reading your logs ${DIM("· on this computer, offline")}\n`);
|
|
1072
1121
|
const nWorkers = Math.max(1, Math.min(4, (os.availableParallelism ? os.availableParallelism() : os.cpus().length) - 1));
|
|
1073
1122
|
await new Promise((resolve) => {
|
|
1074
1123
|
let next = 0, active = 0;
|
|
@@ -1077,13 +1126,13 @@ async function main() {
|
|
|
1077
1126
|
const feed = (w) => {
|
|
1078
1127
|
if (next >= jobs.length) { w.terminate(); if (--active === 0) resolve(); return; }
|
|
1079
1128
|
const job = jobs[next++];
|
|
1080
|
-
w.once("message", (r) => { if (r.ok) summaries.push(r.s); done++; doneBytes += job.size;
|
|
1129
|
+
w.once("message", (r) => { if (r.ok) { summaries.push(r.s); if (!r.s.sub && r.s.how !== "script") found_p += r.s.prompts.length; } done++; doneBytes += job.size; progress(); feed(w); });
|
|
1081
1130
|
w.postMessage({ file: job.file, src: job.src, examples: false });
|
|
1082
1131
|
};
|
|
1083
1132
|
workers.forEach((w) => { active++; feed(w); });
|
|
1084
1133
|
});
|
|
1085
|
-
if (
|
|
1086
|
-
console.log(` Read ${jobs.length} files in ${Math.round((Date.now() - t0) / 1000)}s
|
|
1134
|
+
if (TTY) { doneBytes = bytes; progress(true); process.stdout.write("\n"); }
|
|
1135
|
+
console.log(` ${tint("green", "✓")} Read ${nf(jobs.length)} files in ${Math.round((Date.now() - t0) / 1000)}s. ${DIM("Nothing was sent.")}`);
|
|
1087
1136
|
if (flag("--explain")) { explain(summaries); return; }
|
|
1088
1137
|
const m = compute(summaries);
|
|
1089
1138
|
if (!m) { console.log("\n Found the logs but no prompts in them. Nothing to make cards from.\n"); return; }
|
|
@@ -1097,13 +1146,24 @@ async function main() {
|
|
|
1097
1146
|
const examples = [];
|
|
1098
1147
|
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 ""; } })();
|
|
1099
1148
|
fs.writeFileSync(page, pageHTML(m, C, { api: share ? (opt("--api") || API_DEFAULT).replace(/\/+$/, "") : null, examples, suggest: suggest.length >= 3 ? suggest : "" }));
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (
|
|
1149
|
+
const T = TYPES[builderType(m)], sc = hourScore(m), nf2 = (n) => Number(n).toLocaleString("en");
|
|
1150
|
+
const facts = [["blue", nf2(m.sessions.total), "sessions", found.length > 1 ? "in Claude Code and Codex" : "in " + found[0].label]];
|
|
1151
|
+
facts.push(["purple", nf2(m.prompts.total), "prompts", `busiest at ${hourLabel(m.busiestHour)}`]);
|
|
1152
|
+
if (m.parallelMax > 1 && m.parallelMax <= MAX_AT_ONCE) facts.push(["orange", String(m.parallelMax), "agents at once", m.atOnceAt ? "at " + atOnceWhen(m.atOnceAt) : ""]);
|
|
1153
|
+
if (m.shipped && m.shipped.commits) facts.push(["mustard", nf2(m.shipped.commits), "commits", m.shipped.prs ? `and ${nf2(m.shipped.prs)} pull request${m.shipped.prs > 1 ? "s" : ""}, made by your agents` : "made by your agents"]);
|
|
1154
|
+
const w = Math.max(...facts.map(([, n, l]) => n.length + 1 + l.length)) + 3;
|
|
1155
|
+
console.log("");
|
|
1156
|
+
for (const [h, n, l, d] of facts) { console.log(` ${tint(h, "●")} ${B(n)} ${l}${" ".repeat(w - n.length - 1 - l.length)}${DIM(d)}`); if (TTY) await sleep(140); }
|
|
1157
|
+
console.log(`\n You're ${grad(T.name + ".", true)}`);
|
|
1158
|
+
const first = String(T.line || "").split(/(?<=\.)\s/)[0];
|
|
1159
|
+
console.log(` ${DIM(first + (sc ? " Hour score" : ""))}${sc ? " " + B(String(sc.total)) + DIM(".") : ""}`);
|
|
1160
|
+
const open = !flag("--no-open");
|
|
1161
|
+
console.log(`\n ${B((open ? "→ Opening your " : "Your ") + C.length + " cards" + (open ? " in the browser" : " are ready"))}`);
|
|
1162
|
+
console.log(` ${DIM(page.startsWith(home) ? "~" + page.slice(home.length) : page)}\n`);
|
|
1163
|
+
if (open) openFile(page);
|
|
1104
1164
|
if (share) {
|
|
1105
|
-
console.log(` Want a profile? The last card lets you claim
|
|
1106
|
-
console.log(` Nothing is sent unless you press the buttons there. You see exactly what goes first
|
|
1165
|
+
console.log(` ${DIM("Want a profile? The last card lets you claim")} usehour.com/@you${DIM(".")}`);
|
|
1166
|
+
console.log(` ${DIM("Nothing is sent unless you press the buttons there. You see exactly what goes first.")}`);
|
|
1107
1167
|
if (examples.length) console.log(DIM(` (The cards page holds ${examples.length} of your prompts so you can choose. It stays on this computer.)`));
|
|
1108
1168
|
console.log("");
|
|
1109
1169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usehour",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
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" },
|