hacklab 26.921.1 → 26.921.3
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 +17 -27
- package/dist/index.js +176 -90
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -1,46 +1,36 @@
|
|
|
1
1
|
# Hacklab CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Requires Bun 1.4.2 or newer.
|
|
3
|
+
Sync coding agent usage to Hacklab. Requires Node.js 22+ or Bun 1.4.2+.
|
|
6
4
|
|
|
7
5
|
```sh
|
|
8
|
-
|
|
9
|
-
hacklab
|
|
6
|
+
npm install --global hacklab@beta
|
|
7
|
+
hacklab sync
|
|
10
8
|
```
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Preview a profile
|
|
10
|
+
The first sync opens your browser to sign in and authorize the CLI. Check that the code matches your terminal before approving. Later commands reuse saved authorization until it expires or is revoked.
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
## Commands
|
|
17
13
|
|
|
18
14
|
```sh
|
|
19
|
-
hacklab
|
|
15
|
+
hacklab sync # Sign in and upload usage
|
|
16
|
+
hacklab login # Sign in without uploading
|
|
17
|
+
hacklab logout # Clear saved authorization
|
|
18
|
+
hacklab --help
|
|
19
|
+
hacklab --version
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Upload after approval
|
|
25
|
-
|
|
26
|
-
Review the exact bio and links first. Agents must ask the user for approval before using `--confirm-upload`.
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
hacklab onboard --file /path/to/profile.json --confirm-upload
|
|
30
|
-
```
|
|
22
|
+
The CLI runs bundled `ccusage@20.0.24 session --json` and uploads its output to `/api/token-usage`. Reports include session identifiers, tokens, costs, model breakdowns, and metadata that may include project paths. Source code and transcripts are not uploaded. Reports are limited to 20 MB. Repeated syncs update existing sessions; sessions from other machines are retained.
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
Credentials are stored per server in `~/.config/hacklab/credentials/`, with owner-only permissions on Unix. Logout removes the local credential for the current server; it does not sign out your browser or revoke the server session.
|
|
33
25
|
|
|
34
|
-
##
|
|
26
|
+
## Development
|
|
35
27
|
|
|
36
|
-
|
|
28
|
+
Building requires Bun 1.4.2+.
|
|
37
29
|
|
|
38
30
|
```sh
|
|
39
31
|
bun install --frozen-lockfile
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
bun link
|
|
43
|
-
hacklab onboard --file /path/to/profile.json --url http://localhost:3000
|
|
32
|
+
bun run --cwd packages/cli build:dev
|
|
33
|
+
bun run --cwd packages/cli start --help
|
|
44
34
|
```
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
Source runs and development builds use `http://localhost:3000`. Production builds embed `BETTER_AUTH_URL` from the build environment or `apps/web/.env` / `.env.local`; it must be an HTTPS origin. No `--url` override is supported. Development and production credentials are stored separately.
|
package/dist/index.js
CHANGED
|
@@ -1,129 +1,215 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// @bun
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/index.ts
|
|
5
|
-
import { parseArgs } from "util";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
6
5
|
// package.json
|
|
7
|
-
var version = "26.921.
|
|
6
|
+
var version = "26.921.3";
|
|
8
7
|
|
|
9
|
-
// src/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
11
|
+
|
|
12
|
+
// src/credentials.ts
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
var credentialsDirectory = join(homedir(), ".config", "hacklab", "credentials");
|
|
18
|
+
function credentialPath(origin, directory) {
|
|
19
|
+
return join(directory, `${createHash("sha256").update(origin).digest("hex")}.json`);
|
|
16
20
|
}
|
|
17
|
-
async function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error("
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
async function readCredential(origin, directory = credentialsDirectory) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(await readFile(credentialPath(origin, directory), "utf8"));
|
|
24
|
+
return value?.origin === origin && typeof value.token === "string" && value.token.length > 0 ? value.token : null;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof SyntaxError || error.code === "ENOENT")
|
|
27
|
+
return null;
|
|
28
|
+
throw new Error("Could not read saved Hacklab authorization.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function saveCredential(origin, token, directory = credentialsDirectory) {
|
|
32
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
33
|
+
await chmod(directory, 448);
|
|
34
|
+
const path = credentialPath(origin, directory);
|
|
35
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(temporary, JSON.stringify({ origin, token }), { mode: 384, flag: "wx" });
|
|
38
|
+
await rename(temporary, path);
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(temporary, { force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function removeCredential(origin, directory = credentialsDirectory) {
|
|
44
|
+
await rm(credentialPath(origin, directory), { force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/auth.ts
|
|
48
|
+
var clientId = "hacklab-cli";
|
|
49
|
+
function openBrowser(url) {
|
|
50
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
51
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
52
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
53
|
+
child.on("error", () => console.log("Open the authorization URL above in your browser."));
|
|
54
|
+
child.unref();
|
|
55
|
+
}
|
|
56
|
+
async function authenticate(origin, launchBrowser = openBrowser, directory = credentialsDirectory) {
|
|
57
|
+
const saved = await readCredential(origin, directory);
|
|
58
|
+
if (saved) {
|
|
59
|
+
const response = await fetch(`${origin}/api/auth/get-session`, {
|
|
60
|
+
headers: { Authorization: `Bearer ${saved}` },
|
|
61
|
+
redirect: "error",
|
|
62
|
+
signal: AbortSignal.timeout(30000)
|
|
63
|
+
});
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
const session = await response.json();
|
|
66
|
+
if (session?.session?.id && session.user?.id)
|
|
67
|
+
return saved;
|
|
68
|
+
if (session !== null)
|
|
69
|
+
throw new Error("Invalid saved authorization response. Try again later.");
|
|
70
|
+
} else if (response.status !== 401) {
|
|
71
|
+
throw new Error(`Could not verify saved Hacklab authorization (HTTP ${response.status}). Try again later.`);
|
|
45
72
|
}
|
|
73
|
+
await removeCredential(origin, directory);
|
|
74
|
+
console.log("Your saved Hacklab authorization expired or was revoked. Sign in again.");
|
|
46
75
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
const request = (path, body) => fetch(`${origin}/api/auth${path}`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
redirect: "error",
|
|
81
|
+
signal: AbortSignal.timeout(30000)
|
|
82
|
+
});
|
|
83
|
+
const response = await request("/device/code", { client_id: clientId });
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw new Error(`Could not start CLI sign-in (HTTP ${response.status}).`);
|
|
86
|
+
const code = await response.json();
|
|
87
|
+
const verification = new URL(code.verification_uri_complete, origin);
|
|
88
|
+
if (verification.origin !== origin || !code.device_code || !code.user_code || !Number.isFinite(code.expires_in) || code.expires_in <= 0 || !Number.isFinite(code.interval) || code.interval <= 0) {
|
|
89
|
+
throw new Error("Invalid CLI authorization response.");
|
|
53
90
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
console.log(`Opening your browser to sign in to Hacklab.
|
|
92
|
+
If it does not open, visit: ${verification.href}
|
|
93
|
+
Waiting for you to connect your account…`);
|
|
94
|
+
launchBrowser(verification.href);
|
|
95
|
+
const deadline = Date.now() + code.expires_in * 1000;
|
|
96
|
+
let interval = code.interval * 1000;
|
|
97
|
+
while (Date.now() < deadline) {
|
|
98
|
+
await sleep(Math.min(interval, deadline - Date.now()));
|
|
99
|
+
if (Date.now() >= deadline)
|
|
100
|
+
break;
|
|
101
|
+
const tokenResponse = await request("/device/token", {
|
|
102
|
+
client_id: clientId,
|
|
103
|
+
device_code: code.device_code,
|
|
104
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
105
|
+
});
|
|
106
|
+
const result = await tokenResponse.json();
|
|
107
|
+
if (tokenResponse.ok && result.access_token) {
|
|
108
|
+
await saveCredential(origin, result.access_token, directory);
|
|
109
|
+
return result.access_token;
|
|
110
|
+
}
|
|
111
|
+
if (result.error === "authorization_pending")
|
|
112
|
+
continue;
|
|
113
|
+
if (result.error === "slow_down") {
|
|
114
|
+
interval += 5000;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (result.error === "access_denied")
|
|
118
|
+
throw new Error("CLI authorization was denied. Nothing uploaded.");
|
|
119
|
+
if (result.error === "expired_token")
|
|
120
|
+
break;
|
|
121
|
+
throw new Error(`CLI sign-in failed (HTTP ${tokenResponse.status}). Run the command again to retry.`);
|
|
122
|
+
}
|
|
123
|
+
throw new Error("CLI sign-in expired. Run the command again to retry.");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/origin.ts
|
|
127
|
+
function cliOrigin() {
|
|
128
|
+
return "https://hacklab.so";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/sync.ts
|
|
132
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
133
|
+
import { once } from "node:events";
|
|
134
|
+
import { createRequire } from "node:module";
|
|
135
|
+
import { text } from "node:stream/consumers";
|
|
136
|
+
async function collectUsage() {
|
|
137
|
+
const require2 = createRequire(import.meta.url);
|
|
138
|
+
const child = spawn2(process.execPath, [require2.resolve("ccusage/src/cli.js"), "session", "--json"], { stdio: ["ignore", "pipe", "inherit"] });
|
|
139
|
+
const [body, [exitCode]] = await Promise.all([text(child.stdout), once(child, "close")]);
|
|
140
|
+
if (exitCode !== 0)
|
|
141
|
+
throw new Error(`ccusage failed (exit ${exitCode}). Nothing synced.`);
|
|
142
|
+
let report;
|
|
58
143
|
try {
|
|
59
|
-
|
|
144
|
+
report = JSON.parse(body);
|
|
60
145
|
} catch {
|
|
61
|
-
throw new Error("
|
|
146
|
+
throw new Error("ccusage returned invalid JSON. Nothing synced.");
|
|
62
147
|
}
|
|
63
|
-
if (
|
|
64
|
-
throw new Error("
|
|
148
|
+
if (!Array.isArray(report?.session) || !report.totals) {
|
|
149
|
+
throw new Error("Unexpected ccusage report. Nothing synced.");
|
|
65
150
|
}
|
|
66
|
-
|
|
151
|
+
if (Buffer.byteLength(body) > 20 * 1024 * 1024)
|
|
152
|
+
throw new Error("Usage report exceeds 20 MB. Nothing synced.");
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
155
|
+
async function uploadUsage(origin, token, body) {
|
|
156
|
+
const response = await fetch(`${origin}/api/token-usage`, {
|
|
67
157
|
method: "POST",
|
|
68
158
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
69
|
-
body
|
|
159
|
+
body,
|
|
70
160
|
redirect: "error",
|
|
71
|
-
signal: AbortSignal.timeout(
|
|
161
|
+
signal: AbortSignal.timeout(60000)
|
|
72
162
|
});
|
|
73
|
-
if (!response.ok)
|
|
74
|
-
throw new Error(`Hacklab rejected the
|
|
75
|
-
}
|
|
163
|
+
if (!response.ok)
|
|
164
|
+
throw new Error(`Hacklab rejected the usage report (HTTP ${response.status}). Run hacklab sync to retry.`);
|
|
76
165
|
const result = await response.json();
|
|
77
166
|
if (result.ok !== true)
|
|
78
|
-
throw new Error("Hacklab did not confirm the
|
|
79
|
-
console.log(
|
|
167
|
+
throw new Error("Hacklab did not confirm the sync. Run hacklab sync to retry.");
|
|
168
|
+
console.log(`Synced ${result.sessions} usage sessions to Hacklab.`);
|
|
169
|
+
if (result.sessions === 0)
|
|
170
|
+
console.log("No usage found. Use your coding agent, then run hacklab sync again.");
|
|
171
|
+
}
|
|
172
|
+
async function sync() {
|
|
173
|
+
const origin = cliOrigin();
|
|
174
|
+
const token = await authenticate(origin);
|
|
175
|
+
console.log("Collecting ccusage session data…");
|
|
176
|
+
const body = await collectUsage();
|
|
177
|
+
await uploadUsage(origin, token, body);
|
|
80
178
|
}
|
|
81
179
|
|
|
82
180
|
// src/index.ts
|
|
83
181
|
var help = `Hacklab CLI
|
|
84
182
|
|
|
85
183
|
Usage:
|
|
86
|
-
hacklab
|
|
87
|
-
hacklab
|
|
88
|
-
hacklab
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
--file Local JSON containing bio and socials
|
|
92
|
-
--url Hacklab origin (default: https://beta.hacklab.so)
|
|
93
|
-
--confirm-upload Upload only after the user approves the exact profile
|
|
94
|
-
|
|
95
|
-
Without --confirm-upload, onboard validates and previews locally without uploading.
|
|
96
|
-
Uploads require HACKLAB_ONBOARDING_TOKEN from the signed-in onboarding dialog.
|
|
97
|
-
Bun 1.4.2 or newer is required.`;
|
|
184
|
+
hacklab sync Sign in and upload usage
|
|
185
|
+
hacklab login Sign in without uploading
|
|
186
|
+
hacklab logout Clear saved authorization
|
|
187
|
+
hacklab --help Show help
|
|
188
|
+
hacklab --version Show version`;
|
|
98
189
|
try {
|
|
99
190
|
const { values, positionals } = parseArgs({
|
|
100
|
-
args:
|
|
191
|
+
args: process.argv.slice(2),
|
|
101
192
|
options: {
|
|
102
193
|
help: { type: "boolean", short: "h" },
|
|
103
|
-
version: { type: "boolean", short: "v" }
|
|
104
|
-
file: { type: "string" },
|
|
105
|
-
url: { type: "string" },
|
|
106
|
-
"confirm-upload": { type: "boolean" }
|
|
194
|
+
version: { type: "boolean", short: "v" }
|
|
107
195
|
},
|
|
108
196
|
allowPositionals: true
|
|
109
197
|
});
|
|
110
|
-
if (positionals.length > 1 || positionals[0] &&
|
|
111
|
-
throw new Error("Unknown command.
|
|
198
|
+
if (positionals.length > 1 || positionals[0] && !["sync", "login", "logout"].includes(positionals[0])) {
|
|
199
|
+
throw new Error("Unknown command. Run hacklab --help for usage.");
|
|
112
200
|
}
|
|
113
201
|
if (values.help) {
|
|
114
202
|
console.log(help);
|
|
115
203
|
} else if (values.version) {
|
|
116
204
|
console.log(version);
|
|
117
|
-
} else if (positionals[0] === "
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} else if (values.file || values.url || values["confirm-upload"]) {
|
|
126
|
-
throw new Error("Profile options require the onboard command.");
|
|
205
|
+
} else if (positionals[0] === "login") {
|
|
206
|
+
await authenticate(cliOrigin());
|
|
207
|
+
console.log("Signed in to Hacklab. Run hacklab sync to upload usage.");
|
|
208
|
+
} else if (positionals[0] === "logout") {
|
|
209
|
+
await removeCredential(cliOrigin());
|
|
210
|
+
console.log("Saved CLI authorization cleared. Run hacklab sync to sign in again.");
|
|
211
|
+
} else if (positionals[0] === "sync") {
|
|
212
|
+
await sync();
|
|
127
213
|
} else {
|
|
128
214
|
console.log(help);
|
|
129
215
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hacklab",
|
|
3
|
-
"version": "26.921.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "26.921.3",
|
|
4
|
+
"description": "CLI for syncing coding agent usage to Hacklab",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hacklab": "./dist/index.js"
|
|
7
7
|
},
|
|
@@ -14,18 +14,22 @@
|
|
|
14
14
|
"tag": "beta"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "bun
|
|
17
|
+
"build": "bun --env-file=../../apps/web/.env --env-file=../../apps/web/.env.local build.ts",
|
|
18
18
|
"start": "bun src/index.ts",
|
|
19
19
|
"lint": "oxlint .",
|
|
20
20
|
"check-types": "tsc --noEmit",
|
|
21
21
|
"prepack": "bun run build",
|
|
22
|
-
"
|
|
22
|
+
"build:dev": "bun build.ts --development"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"ccusage": "20.0.24"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@types/bun": "
|
|
26
|
-
"
|
|
28
|
+
"@types/bun": "catalog:",
|
|
29
|
+
"@types/node": "catalog:",
|
|
30
|
+
"typescript": "catalog:"
|
|
27
31
|
},
|
|
28
32
|
"engines": {
|
|
29
|
-
"
|
|
33
|
+
"node": ">=22"
|
|
30
34
|
}
|
|
31
35
|
}
|