@basestream/cli 0.1.0 → 0.1.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.
Potentially problematic release.
This version of @basestream/cli might be problematic. Click here for more details.
- package/bin/basestream.js +1 -1
- package/dist/cli.mjs +730 -0
- package/package.json +9 -51
- package/dist/cli/commands/hook-stop.d.ts +0 -1
- package/dist/cli/commands/hook-stop.js +0 -247
- package/dist/cli/commands/init.d.ts +0 -1
- package/dist/cli/commands/init.js +0 -92
- package/dist/cli/commands/login.d.ts +0 -1
- package/dist/cli/commands/login.js +0 -75
- package/dist/cli/commands/status.d.ts +0 -1
- package/dist/cli/commands/status.js +0 -97
- package/dist/cli/commands/sync.d.ts +0 -7
- package/dist/cli/commands/sync.js +0 -77
- package/dist/cli/config.d.ts +0 -12
- package/dist/cli/config.js +0 -31
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -57
- package/dist/cli/util.d.ts +0 -11
- package/dist/cli/util.js +0 -18
package/package.json
CHANGED
|
@@ -1,64 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@basestream/cli",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "AI work intelligence for teams — automatic work tracking for Claude Code",
|
|
5
|
-
"type": "module",
|
|
6
2
|
"bin": {
|
|
7
|
-
"basestream": "
|
|
3
|
+
"basestream": "bin/basestream.js"
|
|
8
4
|
},
|
|
5
|
+
"description": "AI work intelligence for teams — automatic work tracking for Claude Code",
|
|
9
6
|
"files": [
|
|
10
7
|
"bin/",
|
|
11
|
-
"dist/cli
|
|
8
|
+
"dist/cli.mjs",
|
|
12
9
|
"README.md"
|
|
13
10
|
],
|
|
11
|
+
"name": "@basestream/cli",
|
|
14
12
|
"scripts": {
|
|
15
|
-
"
|
|
16
|
-
"build": "next build",
|
|
17
|
-
"build:cli": "tsc -p tsconfig.cli.json",
|
|
13
|
+
"build:cli": "node scripts/build-cli.mjs",
|
|
18
14
|
"dev:cli": "tsc -p tsconfig.cli.json --watch",
|
|
19
|
-
"test:cli": "tsc -p tsconfig.cli.json && node bin/basestream.js",
|
|
20
15
|
"link:cli": "pnpm run build:cli && npm link",
|
|
21
|
-
"unlink:cli": "npm unlink -g @basestream/cli",
|
|
22
16
|
"prebuild:publish": "npm run build:cli",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"db:generate": "drizzle-kit generate",
|
|
26
|
-
"db:migrate": "drizzle-kit migrate",
|
|
27
|
-
"db:push": "drizzle-kit push",
|
|
28
|
-
"db:seed": "tsx src/db/seed.ts"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@neondatabase/serverless": "^1.0.2",
|
|
32
|
-
"@octokit/auth-app": "^8.2.0",
|
|
33
|
-
"@octokit/rest": "^22.0.1",
|
|
34
|
-
"@paralleldrive/cuid2": "^2.2.2",
|
|
35
|
-
"@stripe/stripe-js": "^3.0.0",
|
|
36
|
-
"better-auth": "^1.6.0",
|
|
37
|
-
"dotenv": "^17.4.1",
|
|
38
|
-
"drizzle-orm": "^0.45.2",
|
|
39
|
-
"next": "^16.2.2",
|
|
40
|
-
"pg": "^8.20.0",
|
|
41
|
-
"react": "^19.2.4",
|
|
42
|
-
"react-dom": "^19.2.4",
|
|
43
|
-
"react-markdown": "^10.1.0",
|
|
44
|
-
"recharts": "^2.12.7",
|
|
45
|
-
"stripe": "^15.0.0",
|
|
46
|
-
"swr": "^2.4.1",
|
|
47
|
-
"zod": "^4.3.6"
|
|
17
|
+
"test:cli": "tsc -p tsconfig.cli.json && node bin/basestream.js",
|
|
18
|
+
"unlink:cli": "npm unlink -g @basestream/cli"
|
|
48
19
|
},
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
"@types/node": "^25.5.2",
|
|
52
|
-
"@types/pg": "^8.20.0",
|
|
53
|
-
"@types/react": "^19.2.14",
|
|
54
|
-
"@types/react-dom": "^19.2.3",
|
|
55
|
-
"@eslint/eslintrc": "^3",
|
|
56
|
-
"drizzle-kit": "^0.31.10",
|
|
57
|
-
"eslint": "^9",
|
|
58
|
-
"eslint-config-next": "16.2.2",
|
|
59
|
-
"postcss": "^8.4.38",
|
|
60
|
-
"tailwindcss": "^4.2.2",
|
|
61
|
-
"tsx": "^4.19.0",
|
|
62
|
-
"typescript": "^5.4.5"
|
|
63
|
-
}
|
|
20
|
+
"type": "module",
|
|
21
|
+
"version": "0.1.3"
|
|
64
22
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function hookStop(): Promise<void>;
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
|
-
import { BUFFER_DIR, SESSION_DIR, ensureDirs, readConfig } from "../config.js";
|
|
5
|
-
function parseTranscriptLine(line) {
|
|
6
|
-
try {
|
|
7
|
-
return JSON.parse(line);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function extractGitInfo(cwd) {
|
|
14
|
-
const result = {};
|
|
15
|
-
try {
|
|
16
|
-
result.branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
17
|
-
cwd,
|
|
18
|
-
encoding: "utf-8",
|
|
19
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
-
}).trim();
|
|
21
|
-
}
|
|
22
|
-
catch { }
|
|
23
|
-
try {
|
|
24
|
-
result.repo = execSync("git remote get-url origin", {
|
|
25
|
-
cwd,
|
|
26
|
-
encoding: "utf-8",
|
|
27
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
-
}).trim();
|
|
29
|
-
}
|
|
30
|
-
catch { }
|
|
31
|
-
result.projectName = path.basename(cwd);
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
|
-
function readSessionAccumulator(sessionId) {
|
|
35
|
-
const sessionFile = path.join(SESSION_DIR, `${sessionId}.json`);
|
|
36
|
-
if (!fs.existsSync(sessionFile))
|
|
37
|
-
return null;
|
|
38
|
-
try {
|
|
39
|
-
const raw = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
|
|
40
|
-
raw.filesWritten = new Set(raw.filesWritten || []);
|
|
41
|
-
raw.commitShas = new Set(raw.commitShas || []);
|
|
42
|
-
return raw;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function writeSessionAccumulator(acc) {
|
|
49
|
-
const sessionFile = path.join(SESSION_DIR, `${acc.sessionId}.json`);
|
|
50
|
-
const serializable = {
|
|
51
|
-
...acc,
|
|
52
|
-
filesWritten: Array.from(acc.filesWritten),
|
|
53
|
-
commitShas: Array.from(acc.commitShas),
|
|
54
|
-
};
|
|
55
|
-
fs.writeFileSync(sessionFile, JSON.stringify(serializable, null, 2));
|
|
56
|
-
}
|
|
57
|
-
function analyzeTranscript(transcriptPath, acc) {
|
|
58
|
-
if (!fs.existsSync(transcriptPath))
|
|
59
|
-
return acc;
|
|
60
|
-
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
61
|
-
const lines = content.split("\n").filter(Boolean);
|
|
62
|
-
for (const line of lines) {
|
|
63
|
-
const msg = parseTranscriptLine(line);
|
|
64
|
-
if (!msg)
|
|
65
|
-
continue;
|
|
66
|
-
// Count assistant turns
|
|
67
|
-
if (msg.role === "assistant") {
|
|
68
|
-
acc.turns++;
|
|
69
|
-
}
|
|
70
|
-
// Track tool usage
|
|
71
|
-
if (msg.type === "tool_use" || msg.type === "tool_call") {
|
|
72
|
-
const toolName = (msg).name ||
|
|
73
|
-
msg.tool_name ||
|
|
74
|
-
"unknown";
|
|
75
|
-
acc.toolCalls.push({ tool: toolName, timestamp: msg.timestamp });
|
|
76
|
-
// Extract file paths from Write/Edit tool calls
|
|
77
|
-
if (toolName === "Write" ||
|
|
78
|
-
toolName === "Edit" ||
|
|
79
|
-
toolName === "NotebookEdit") {
|
|
80
|
-
const input = (msg).input;
|
|
81
|
-
const filePath = input?.file_path || input?.path;
|
|
82
|
-
if (filePath)
|
|
83
|
-
acc.filesWritten.add(filePath);
|
|
84
|
-
}
|
|
85
|
-
// Extract commit SHAs from Bash tool calls
|
|
86
|
-
if (toolName === "Bash") {
|
|
87
|
-
const input = (msg).input;
|
|
88
|
-
const cmd = input?.command || "";
|
|
89
|
-
if (cmd.includes("git commit")) {
|
|
90
|
-
// Look ahead for the result containing the SHA
|
|
91
|
-
// We'll catch it in the tool_result handler
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Extract commit SHAs from tool results
|
|
96
|
-
if (msg.type === "tool_result") {
|
|
97
|
-
const resultContent = String((msg).content || "");
|
|
98
|
-
const shaMatch = resultContent.match(/\[([a-f0-9]{7,12})\]\s/);
|
|
99
|
-
if (shaMatch) {
|
|
100
|
-
acc.commitShas.add(shaMatch[1]);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return acc;
|
|
105
|
-
}
|
|
106
|
-
function categorizeWork(acc) {
|
|
107
|
-
const files = Array.from(acc.filesWritten);
|
|
108
|
-
const fileCount = files.length;
|
|
109
|
-
// Categorize based on file patterns
|
|
110
|
-
let category = "OTHER";
|
|
111
|
-
const testFiles = files.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__"));
|
|
112
|
-
const docFiles = files.filter((f) => f.endsWith(".md") || f.includes("/docs/"));
|
|
113
|
-
const configFiles = files.filter((f) => f.includes("Dockerfile") ||
|
|
114
|
-
f.includes(".yml") ||
|
|
115
|
-
f.includes(".yaml") ||
|
|
116
|
-
f.includes("ci"));
|
|
117
|
-
if (testFiles.length > fileCount / 2)
|
|
118
|
-
category = "TESTING";
|
|
119
|
-
else if (docFiles.length > fileCount / 2)
|
|
120
|
-
category = "DOCS";
|
|
121
|
-
else if (configFiles.length > fileCount / 2)
|
|
122
|
-
category = "DEVOPS";
|
|
123
|
-
else if (fileCount > 0)
|
|
124
|
-
category = "FEATURE";
|
|
125
|
-
// Complexity
|
|
126
|
-
let complexity = "LOW";
|
|
127
|
-
if (fileCount === 0)
|
|
128
|
-
complexity = "LOW";
|
|
129
|
-
else if (fileCount <= 2)
|
|
130
|
-
complexity = "LOW";
|
|
131
|
-
else if (fileCount <= 6)
|
|
132
|
-
complexity = "MEDIUM";
|
|
133
|
-
else
|
|
134
|
-
complexity = "HIGH";
|
|
135
|
-
return { category, complexity };
|
|
136
|
-
}
|
|
137
|
-
function flushToBuffer(acc) {
|
|
138
|
-
const { category, complexity } = categorizeWork(acc);
|
|
139
|
-
const now = new Date();
|
|
140
|
-
const startTime = new Date(acc.startedAt);
|
|
141
|
-
const durationMin = Math.round((now.getTime() - startTime.getTime()) / 60_000);
|
|
142
|
-
const entry = {
|
|
143
|
-
sessionId: acc.sessionId,
|
|
144
|
-
tool: "claude-code",
|
|
145
|
-
projectName: acc.projectName || path.basename(acc.cwd),
|
|
146
|
-
repo: acc.gitRepo || null,
|
|
147
|
-
branch: acc.gitBranch || null,
|
|
148
|
-
summary: buildSummary(acc, category),
|
|
149
|
-
category,
|
|
150
|
-
why: null, // Hook-based: no "why" available (CLAUDE.md rules would provide this)
|
|
151
|
-
whatChanged: Array.from(acc.filesWritten).map((f) => path.relative(acc.cwd, f) || f),
|
|
152
|
-
outcome: "IN_PROGRESS",
|
|
153
|
-
filesTouched: acc.filesWritten.size,
|
|
154
|
-
complexity,
|
|
155
|
-
toolVersion: null,
|
|
156
|
-
model: null,
|
|
157
|
-
sessionDurationMin: durationMin > 0 ? durationMin : 1,
|
|
158
|
-
turns: acc.turns,
|
|
159
|
-
prUrl: null,
|
|
160
|
-
ticketUrl: null,
|
|
161
|
-
commitShas: Array.from(acc.commitShas),
|
|
162
|
-
visibility: "TEAM",
|
|
163
|
-
bufferedAt: now.toISOString(),
|
|
164
|
-
};
|
|
165
|
-
ensureDirs();
|
|
166
|
-
const filename = `${Date.now()}-${acc.sessionId.slice(0, 8)}.json`;
|
|
167
|
-
fs.writeFileSync(path.join(BUFFER_DIR, filename), JSON.stringify(entry, null, 2));
|
|
168
|
-
}
|
|
169
|
-
function buildSummary(acc, category) {
|
|
170
|
-
const fileCount = acc.filesWritten.size;
|
|
171
|
-
const commitCount = acc.commitShas.size;
|
|
172
|
-
const parts = [];
|
|
173
|
-
if (category !== "OTHER") {
|
|
174
|
-
parts.push(category.toLowerCase());
|
|
175
|
-
}
|
|
176
|
-
if (fileCount > 0) {
|
|
177
|
-
parts.push(`${fileCount} file${fileCount !== 1 ? "s" : ""} modified`);
|
|
178
|
-
}
|
|
179
|
-
if (commitCount > 0) {
|
|
180
|
-
parts.push(`${commitCount} commit${commitCount !== 1 ? "s" : ""}`);
|
|
181
|
-
}
|
|
182
|
-
if (acc.projectName) {
|
|
183
|
-
parts.push(`in ${acc.projectName}`);
|
|
184
|
-
}
|
|
185
|
-
return parts.length > 0
|
|
186
|
-
? parts.join(", ")
|
|
187
|
-
: `Claude Code session in ${acc.projectName || "unknown project"}`;
|
|
188
|
-
}
|
|
189
|
-
export async function hookStop() {
|
|
190
|
-
// Read hook payload from stdin
|
|
191
|
-
let payload;
|
|
192
|
-
try {
|
|
193
|
-
const chunks = [];
|
|
194
|
-
for await (const chunk of process.stdin) {
|
|
195
|
-
chunks.push(chunk);
|
|
196
|
-
}
|
|
197
|
-
payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// No valid payload — nothing to do
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
|
-
const { session_id, transcript_path, cwd } = payload;
|
|
204
|
-
if (!session_id)
|
|
205
|
-
process.exit(0);
|
|
206
|
-
ensureDirs();
|
|
207
|
-
// Load or create session accumulator
|
|
208
|
-
let acc = readSessionAccumulator(session_id);
|
|
209
|
-
if (!acc) {
|
|
210
|
-
const gitInfo = extractGitInfo(cwd);
|
|
211
|
-
acc = {
|
|
212
|
-
sessionId: session_id,
|
|
213
|
-
cwd,
|
|
214
|
-
startedAt: new Date().toISOString(),
|
|
215
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
216
|
-
turns: 0,
|
|
217
|
-
toolCalls: [],
|
|
218
|
-
filesWritten: new Set(),
|
|
219
|
-
commitShas: new Set(),
|
|
220
|
-
gitBranch: gitInfo.branch,
|
|
221
|
-
gitRepo: gitInfo.repo,
|
|
222
|
-
projectName: gitInfo.projectName,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
// Analyze transcript to accumulate data
|
|
226
|
-
if (transcript_path) {
|
|
227
|
-
acc = analyzeTranscript(transcript_path, acc);
|
|
228
|
-
}
|
|
229
|
-
acc.lastUpdatedAt = new Date().toISOString();
|
|
230
|
-
// Save accumulator for future Stop events in this session
|
|
231
|
-
writeSessionAccumulator(acc);
|
|
232
|
-
// Flush to buffer if we have meaningful work
|
|
233
|
-
if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 3) {
|
|
234
|
-
flushToBuffer(acc);
|
|
235
|
-
// Auto-sync if config exists
|
|
236
|
-
const config = readConfig();
|
|
237
|
-
if (config?.apiKey) {
|
|
238
|
-
try {
|
|
239
|
-
const { syncEntries } = await import("./sync.js");
|
|
240
|
-
await syncEntries(config);
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// Sync failure is non-fatal — entries stay in buffer
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function init(): Promise<void>;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
import { ensureDirs, readConfig } from "../config.js";
|
|
6
|
-
import { c, check, warn } from "../util.js";
|
|
7
|
-
import { login } from "./login.js";
|
|
8
|
-
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
9
|
-
const HOOK_COMMAND = "npx @basestream/cli _hook-stop";
|
|
10
|
-
function detectClaudeCode() {
|
|
11
|
-
try {
|
|
12
|
-
const version = execSync("claude --version 2>/dev/null", {
|
|
13
|
-
encoding: "utf-8",
|
|
14
|
-
}).trim();
|
|
15
|
-
return version || null;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function injectClaudeCodeHook() {
|
|
22
|
-
const settingsDir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
23
|
-
fs.mkdirSync(settingsDir, { recursive: true });
|
|
24
|
-
let settings = {};
|
|
25
|
-
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
26
|
-
try {
|
|
27
|
-
settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// Corrupted settings — start fresh
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Initialize hooks structure
|
|
34
|
-
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
35
|
-
settings.hooks = {};
|
|
36
|
-
}
|
|
37
|
-
const hooks = settings.hooks;
|
|
38
|
-
// Check if our hook is already installed
|
|
39
|
-
if (Array.isArray(hooks.Stop)) {
|
|
40
|
-
const existing = hooks.Stop;
|
|
41
|
-
const alreadyInstalled = existing.some((entry) => entry.hooks?.some((h) => h.command?.includes("@basestream/cli")));
|
|
42
|
-
if (alreadyInstalled) {
|
|
43
|
-
check("Claude Code hook already installed");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Add our Stop hook
|
|
48
|
-
if (!Array.isArray(hooks.Stop)) {
|
|
49
|
-
hooks.Stop = [];
|
|
50
|
-
}
|
|
51
|
-
hooks.Stop.push({
|
|
52
|
-
matcher: "*",
|
|
53
|
-
hooks: [
|
|
54
|
-
{
|
|
55
|
-
type: "command",
|
|
56
|
-
command: HOOK_COMMAND,
|
|
57
|
-
timeout: 30,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
});
|
|
61
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
62
|
-
check("Injected tracking hook into ~/.claude/settings.json");
|
|
63
|
-
}
|
|
64
|
-
export async function init() {
|
|
65
|
-
console.log();
|
|
66
|
-
// 1. Detect Claude Code
|
|
67
|
-
const ccVersion = detectClaudeCode();
|
|
68
|
-
if (ccVersion) {
|
|
69
|
-
console.log(` ${c.dim(`Detected: Claude Code ${ccVersion}`)}`);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
warn("Claude Code not detected — hook will activate when installed");
|
|
73
|
-
}
|
|
74
|
-
console.log();
|
|
75
|
-
// 2. Inject hook
|
|
76
|
-
injectClaudeCodeHook();
|
|
77
|
-
// 3. Create buffer directory
|
|
78
|
-
ensureDirs();
|
|
79
|
-
check(`Created ~/.basestream/buffer/`);
|
|
80
|
-
// 4. Authenticate
|
|
81
|
-
const existing = readConfig();
|
|
82
|
-
if (existing?.apiKey) {
|
|
83
|
-
check("Already authenticated");
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
console.log();
|
|
87
|
-
await login();
|
|
88
|
-
}
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(` ${c.dim("That's it. Work normally — every session is now tracked.")}`);
|
|
91
|
-
console.log();
|
|
92
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function login(): Promise<string>;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { writeConfig, readConfig } from "../config.js";
|
|
4
|
-
import { c, check } from "../util.js";
|
|
5
|
-
const DEFAULT_BASE_URL = "https://basestream.ai";
|
|
6
|
-
function openBrowser(url) {
|
|
7
|
-
try {
|
|
8
|
-
const platform = process.platform;
|
|
9
|
-
if (platform === "darwin")
|
|
10
|
-
execSync(`open "${url}"`);
|
|
11
|
-
else if (platform === "win32")
|
|
12
|
-
execSync(`start "${url}"`);
|
|
13
|
-
else
|
|
14
|
-
execSync(`xdg-open "${url}"`);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
console.log(` ${c.dim(`Open this URL in your browser: ${url}`)}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export async function login() {
|
|
21
|
-
const existing = readConfig();
|
|
22
|
-
const baseUrl = existing?.baseUrl ||
|
|
23
|
-
process.env.BASESTREAM_URL ||
|
|
24
|
-
DEFAULT_BASE_URL;
|
|
25
|
-
console.log(` ${c.dim("Opening browser for authentication...")}`);
|
|
26
|
-
// Start a temporary local server to receive the API key callback
|
|
27
|
-
const apiKey = await new Promise((resolve, reject) => {
|
|
28
|
-
const server = http.createServer((req, res) => {
|
|
29
|
-
const url = new URL(req.url || "/", `http://localhost`);
|
|
30
|
-
const key = url.searchParams.get("key");
|
|
31
|
-
const orgId = url.searchParams.get("orgId");
|
|
32
|
-
if (key) {
|
|
33
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
34
|
-
res.end(`
|
|
35
|
-
<html>
|
|
36
|
-
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
|
|
37
|
-
<div style="text-align: center;">
|
|
38
|
-
<h1 style="color: #22c55e;">✓ Authenticated</h1>
|
|
39
|
-
<p style="color: #a1a1aa;">You can close this tab and return to your terminal.</p>
|
|
40
|
-
</div>
|
|
41
|
-
</body>
|
|
42
|
-
</html>
|
|
43
|
-
`);
|
|
44
|
-
server.close();
|
|
45
|
-
writeConfig({
|
|
46
|
-
apiKey: key,
|
|
47
|
-
baseUrl,
|
|
48
|
-
orgId: orgId || undefined,
|
|
49
|
-
});
|
|
50
|
-
resolve(key);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
res.writeHead(400);
|
|
54
|
-
res.end("Missing key parameter");
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
server.listen(0, "127.0.0.1", () => {
|
|
58
|
-
const addr = server.address();
|
|
59
|
-
if (!addr || typeof addr === "string") {
|
|
60
|
-
reject(new Error("Failed to start callback server"));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const callbackPort = addr.port;
|
|
64
|
-
const authUrl = `${baseUrl}/api/auth/cli?callback=http://127.0.0.1:${callbackPort}`;
|
|
65
|
-
openBrowser(authUrl);
|
|
66
|
-
});
|
|
67
|
-
// Timeout after 2 minutes
|
|
68
|
-
setTimeout(() => {
|
|
69
|
-
server.close();
|
|
70
|
-
reject(new Error("Authentication timed out"));
|
|
71
|
-
}, 120_000);
|
|
72
|
-
});
|
|
73
|
-
check("Authenticated with Basestream");
|
|
74
|
-
return apiKey;
|
|
75
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function status(): Promise<void>;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { BUFFER_DIR, ensureDirs, readConfig } from "../config.js";
|
|
4
|
-
import { c } from "../util.js";
|
|
5
|
-
function groupBy(arr, fn) {
|
|
6
|
-
const result = {};
|
|
7
|
-
for (const item of arr) {
|
|
8
|
-
const key = fn(item);
|
|
9
|
-
(result[key] ??= []).push(item);
|
|
10
|
-
}
|
|
11
|
-
return result;
|
|
12
|
-
}
|
|
13
|
-
function formatOutcome(outcome) {
|
|
14
|
-
switch (outcome) {
|
|
15
|
-
case "COMPLETED":
|
|
16
|
-
return c.green("completed");
|
|
17
|
-
case "IN_PROGRESS":
|
|
18
|
-
return c.yellow("in progress");
|
|
19
|
-
case "BLOCKED":
|
|
20
|
-
return c.red("blocked");
|
|
21
|
-
default:
|
|
22
|
-
return c.dim(outcome.toLowerCase());
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function fetchRemoteEntries(config) {
|
|
26
|
-
try {
|
|
27
|
-
const res = await fetch(`${config.baseUrl}/api/entries?period=week&limit=100`, {
|
|
28
|
-
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
29
|
-
});
|
|
30
|
-
if (!res.ok)
|
|
31
|
-
return [];
|
|
32
|
-
const data = (await res.json());
|
|
33
|
-
return data.entries || [];
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function readLocalEntries() {
|
|
40
|
-
ensureDirs();
|
|
41
|
-
const files = fs
|
|
42
|
-
.readdirSync(BUFFER_DIR)
|
|
43
|
-
.filter((f) => f.endsWith(".json"));
|
|
44
|
-
const entries = [];
|
|
45
|
-
for (const file of files) {
|
|
46
|
-
try {
|
|
47
|
-
entries.push(JSON.parse(fs.readFileSync(path.join(BUFFER_DIR, file), "utf-8")));
|
|
48
|
-
}
|
|
49
|
-
catch { }
|
|
50
|
-
}
|
|
51
|
-
return entries;
|
|
52
|
-
}
|
|
53
|
-
export async function status() {
|
|
54
|
-
const config = readConfig();
|
|
55
|
-
// Combine local + remote entries
|
|
56
|
-
const entries = readLocalEntries();
|
|
57
|
-
if (config?.apiKey) {
|
|
58
|
-
const remote = await fetchRemoteEntries(config);
|
|
59
|
-
// Merge, dedup by sessionId
|
|
60
|
-
const seen = new Set(entries.map((e) => e.sessionId).filter(Boolean));
|
|
61
|
-
for (const re of remote) {
|
|
62
|
-
if (!re.sessionId || !seen.has(re.sessionId)) {
|
|
63
|
-
entries.push(re);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (entries.length === 0) {
|
|
68
|
-
console.log();
|
|
69
|
-
console.log(` ${c.dim("No sessions logged this week.")}`);
|
|
70
|
-
console.log(` ${c.dim("Start using Claude Code — entries will appear here.")}`);
|
|
71
|
-
console.log();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
console.log();
|
|
75
|
-
console.log(` ${c.yellow(`This week — ${entries.length} sessions logged`)}`);
|
|
76
|
-
console.log();
|
|
77
|
-
// Group by project
|
|
78
|
-
const byProject = groupBy(entries, (e) => e.projectName || "unknown");
|
|
79
|
-
for (const [project, projectEntries] of Object.entries(byProject)) {
|
|
80
|
-
const commits = projectEntries.reduce((sum, e) => sum + (e.commitShas?.length || 0), 0);
|
|
81
|
-
const latestOutcome = projectEntries[0]?.outcome || "IN_PROGRESS";
|
|
82
|
-
const outcomeStr = formatOutcome(latestOutcome);
|
|
83
|
-
const parts = [`${projectEntries.length} sessions`];
|
|
84
|
-
if (commits > 0) {
|
|
85
|
-
parts.push(`${commits} commit${commits !== 1 ? "s" : ""}`);
|
|
86
|
-
}
|
|
87
|
-
parts.push(outcomeStr);
|
|
88
|
-
console.log(` ${c.bold(project)} ${c.dim(parts.join(" · "))}`);
|
|
89
|
-
}
|
|
90
|
-
// Show pending sync count
|
|
91
|
-
const localOnly = readLocalEntries();
|
|
92
|
-
if (localOnly.length > 0) {
|
|
93
|
-
console.log();
|
|
94
|
-
console.log(` ${c.dim(`${localOnly.length} entries pending sync`)}`);
|
|
95
|
-
}
|
|
96
|
-
console.log();
|
|
97
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { BUFFER_DIR, ensureDirs, readConfig } from "../config.js";
|
|
4
|
-
import { c, check, fail } from "../util.js";
|
|
5
|
-
export async function syncEntries(config) {
|
|
6
|
-
const cfg = config || readConfig();
|
|
7
|
-
if (!cfg?.apiKey) {
|
|
8
|
-
fail("Not authenticated. Run: basestream login");
|
|
9
|
-
process.exit(1);
|
|
10
|
-
}
|
|
11
|
-
ensureDirs();
|
|
12
|
-
const files = fs
|
|
13
|
-
.readdirSync(BUFFER_DIR)
|
|
14
|
-
.filter((f) => f.endsWith(".json"))
|
|
15
|
-
.sort();
|
|
16
|
-
if (files.length === 0)
|
|
17
|
-
return;
|
|
18
|
-
const entries = [];
|
|
19
|
-
const successFiles = [];
|
|
20
|
-
for (const file of files) {
|
|
21
|
-
try {
|
|
22
|
-
const raw = fs.readFileSync(path.join(BUFFER_DIR, file), "utf-8");
|
|
23
|
-
const entry = JSON.parse(raw);
|
|
24
|
-
entries.push(entry);
|
|
25
|
-
successFiles.push(file);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// Skip corrupt files
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (entries.length === 0)
|
|
32
|
-
return;
|
|
33
|
-
const res = await fetch(`${cfg.baseUrl}/api/sync`, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify({ entries }),
|
|
40
|
-
});
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
const body = await res.text().catch(() => "");
|
|
43
|
-
throw new Error(`Sync failed (${res.status}): ${body}`);
|
|
44
|
-
}
|
|
45
|
-
const result = (await res.json());
|
|
46
|
-
// Clean up synced files
|
|
47
|
-
for (const file of successFiles) {
|
|
48
|
-
fs.unlinkSync(path.join(BUFFER_DIR, file));
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
export async function sync() {
|
|
53
|
-
const cfg = readConfig();
|
|
54
|
-
if (!cfg?.apiKey) {
|
|
55
|
-
fail("Not authenticated. Run: basestream login");
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
ensureDirs();
|
|
59
|
-
const files = fs
|
|
60
|
-
.readdirSync(BUFFER_DIR)
|
|
61
|
-
.filter((f) => f.endsWith(".json"));
|
|
62
|
-
if (files.length === 0) {
|
|
63
|
-
console.log(` ${c.dim("No entries to sync.")}`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
console.log(` ${c.dim(`Syncing ${files.length} entries...`)}`);
|
|
67
|
-
try {
|
|
68
|
-
const result = await syncEntries(cfg);
|
|
69
|
-
if (result) {
|
|
70
|
-
check(`Synced ${result.synced} entries (${result.created} new, ${result.updated} updated)`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
fail(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
}
|