@basestream/cli 0.1.2 → 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.
Potentially problematic release.
This version of @basestream/cli might be problematic. Click here for more details.
- package/dist/cli.mjs +4463 -0
- package/package.json +6 -3
- package/dist/cli/commands/hook-stop.d.ts +0 -1
- package/dist/cli/commands/hook-stop.js +0 -261
- 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 -90
- 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 -1
- package/dist/cli/index.js +0 -60
- package/dist/cli/util.d.ts +0 -11
- package/dist/cli/util.js +0 -18
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"basestream": "bin/basestream.js"
|
|
4
|
+
},
|
|
2
5
|
"description": "AI work intelligence for teams — automatic work tracking for Claude Code",
|
|
3
6
|
"files": [
|
|
4
7
|
"bin/",
|
|
5
|
-
"dist/cli
|
|
8
|
+
"dist/cli.mjs",
|
|
6
9
|
"README.md"
|
|
7
10
|
],
|
|
8
11
|
"name": "@basestream/cli",
|
|
9
12
|
"scripts": {
|
|
10
|
-
"build:cli": "
|
|
13
|
+
"build:cli": "node scripts/build-cli.mjs",
|
|
11
14
|
"dev:cli": "tsc -p tsconfig.cli.json --watch",
|
|
12
15
|
"link:cli": "pnpm run build:cli && npm link",
|
|
13
16
|
"prebuild:publish": "npm run build:cli",
|
|
@@ -15,5 +18,5 @@
|
|
|
15
18
|
"unlink:cli": "npm unlink -g @basestream/cli"
|
|
16
19
|
},
|
|
17
20
|
"type": "module",
|
|
18
|
-
"version": "0.
|
|
21
|
+
"version": "0.2.0"
|
|
19
22
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function hookStop(): Promise<void>;
|
|
@@ -1,261 +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 entry = parseTranscriptLine(line);
|
|
64
|
-
if (!entry)
|
|
65
|
-
continue;
|
|
66
|
-
// Claude Code JSONL format: each line has top-level `type` ("user"|"assistant")
|
|
67
|
-
// and a nested `message` object with `role`, `content` (array of content blocks).
|
|
68
|
-
const message = entry.message;
|
|
69
|
-
const contentBlocks = Array.isArray(message?.content)
|
|
70
|
-
? message.content
|
|
71
|
-
: [];
|
|
72
|
-
// Count assistant turns (only text/thinking content, not pure tool_use to avoid double-counting)
|
|
73
|
-
if (entry.type === "assistant" && contentBlocks.length > 0) {
|
|
74
|
-
const hasText = contentBlocks.some((b) => b.type === "text" || b.type === "thinking");
|
|
75
|
-
if (hasText)
|
|
76
|
-
acc.turns++;
|
|
77
|
-
}
|
|
78
|
-
// Track tool usage from assistant messages
|
|
79
|
-
if (entry.type === "assistant") {
|
|
80
|
-
for (const block of contentBlocks) {
|
|
81
|
-
if (block.type === "tool_use") {
|
|
82
|
-
const toolName = block.name || "unknown";
|
|
83
|
-
acc.toolCalls.push({
|
|
84
|
-
tool: toolName,
|
|
85
|
-
timestamp: entry.timestamp,
|
|
86
|
-
});
|
|
87
|
-
// Extract file paths from Write/Edit tool calls
|
|
88
|
-
if (toolName === "Write" ||
|
|
89
|
-
toolName === "Edit" ||
|
|
90
|
-
toolName === "NotebookEdit") {
|
|
91
|
-
const input = block.input;
|
|
92
|
-
const filePath = input?.file_path || input?.path;
|
|
93
|
-
if (filePath)
|
|
94
|
-
acc.filesWritten.add(filePath);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// Extract commit SHAs from tool results (in user messages)
|
|
100
|
-
if (entry.type === "user") {
|
|
101
|
-
for (const block of contentBlocks) {
|
|
102
|
-
if (block.type === "tool_result") {
|
|
103
|
-
const resultContent = typeof block.content === "string"
|
|
104
|
-
? block.content
|
|
105
|
-
: Array.isArray(block.content)
|
|
106
|
-
? block.content
|
|
107
|
-
.map((c) => c.text || "")
|
|
108
|
-
.join("\n")
|
|
109
|
-
: "";
|
|
110
|
-
const shaMatch = resultContent.match(/\[([a-f0-9]{7,12})\]\s/);
|
|
111
|
-
if (shaMatch) {
|
|
112
|
-
acc.commitShas.add(shaMatch[1]);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return acc;
|
|
119
|
-
}
|
|
120
|
-
function categorizeWork(acc) {
|
|
121
|
-
const files = Array.from(acc.filesWritten);
|
|
122
|
-
const fileCount = files.length;
|
|
123
|
-
// Categorize based on file patterns
|
|
124
|
-
let category = "OTHER";
|
|
125
|
-
const testFiles = files.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__"));
|
|
126
|
-
const docFiles = files.filter((f) => f.endsWith(".md") || f.includes("/docs/"));
|
|
127
|
-
const configFiles = files.filter((f) => f.includes("Dockerfile") ||
|
|
128
|
-
f.includes(".yml") ||
|
|
129
|
-
f.includes(".yaml") ||
|
|
130
|
-
f.includes("ci"));
|
|
131
|
-
if (testFiles.length > fileCount / 2)
|
|
132
|
-
category = "TESTING";
|
|
133
|
-
else if (docFiles.length > fileCount / 2)
|
|
134
|
-
category = "DOCS";
|
|
135
|
-
else if (configFiles.length > fileCount / 2)
|
|
136
|
-
category = "DEVOPS";
|
|
137
|
-
else if (fileCount > 0)
|
|
138
|
-
category = "FEATURE";
|
|
139
|
-
// Complexity
|
|
140
|
-
let complexity = "LOW";
|
|
141
|
-
if (fileCount === 0)
|
|
142
|
-
complexity = "LOW";
|
|
143
|
-
else if (fileCount <= 2)
|
|
144
|
-
complexity = "LOW";
|
|
145
|
-
else if (fileCount <= 6)
|
|
146
|
-
complexity = "MEDIUM";
|
|
147
|
-
else
|
|
148
|
-
complexity = "HIGH";
|
|
149
|
-
return { category, complexity };
|
|
150
|
-
}
|
|
151
|
-
function flushToBuffer(acc) {
|
|
152
|
-
const { category, complexity } = categorizeWork(acc);
|
|
153
|
-
const now = new Date();
|
|
154
|
-
const startTime = new Date(acc.startedAt);
|
|
155
|
-
const durationMin = Math.round((now.getTime() - startTime.getTime()) / 60_000);
|
|
156
|
-
const entry = {
|
|
157
|
-
sessionId: acc.sessionId,
|
|
158
|
-
tool: "claude-code",
|
|
159
|
-
projectName: acc.projectName || path.basename(acc.cwd),
|
|
160
|
-
repo: acc.gitRepo || null,
|
|
161
|
-
branch: acc.gitBranch || null,
|
|
162
|
-
summary: buildSummary(acc, category),
|
|
163
|
-
category,
|
|
164
|
-
why: null, // Hook-based: no "why" available (CLAUDE.md rules would provide this)
|
|
165
|
-
whatChanged: Array.from(acc.filesWritten).map((f) => path.relative(acc.cwd, f) || f),
|
|
166
|
-
outcome: "IN_PROGRESS",
|
|
167
|
-
filesTouched: acc.filesWritten.size,
|
|
168
|
-
complexity,
|
|
169
|
-
toolVersion: null,
|
|
170
|
-
model: null,
|
|
171
|
-
sessionDurationMin: durationMin > 0 ? durationMin : 1,
|
|
172
|
-
turns: acc.turns,
|
|
173
|
-
prUrl: null,
|
|
174
|
-
ticketUrl: null,
|
|
175
|
-
commitShas: Array.from(acc.commitShas),
|
|
176
|
-
visibility: "TEAM",
|
|
177
|
-
bufferedAt: now.toISOString(),
|
|
178
|
-
};
|
|
179
|
-
ensureDirs();
|
|
180
|
-
const filename = `${Date.now()}-${acc.sessionId.slice(0, 8)}.json`;
|
|
181
|
-
fs.writeFileSync(path.join(BUFFER_DIR, filename), JSON.stringify(entry, null, 2));
|
|
182
|
-
}
|
|
183
|
-
function buildSummary(acc, category) {
|
|
184
|
-
const fileCount = acc.filesWritten.size;
|
|
185
|
-
const commitCount = acc.commitShas.size;
|
|
186
|
-
const parts = [];
|
|
187
|
-
if (category !== "OTHER") {
|
|
188
|
-
parts.push(category.toLowerCase());
|
|
189
|
-
}
|
|
190
|
-
if (fileCount > 0) {
|
|
191
|
-
parts.push(`${fileCount} file${fileCount !== 1 ? "s" : ""} modified`);
|
|
192
|
-
}
|
|
193
|
-
if (commitCount > 0) {
|
|
194
|
-
parts.push(`${commitCount} commit${commitCount !== 1 ? "s" : ""}`);
|
|
195
|
-
}
|
|
196
|
-
if (acc.projectName) {
|
|
197
|
-
parts.push(`in ${acc.projectName}`);
|
|
198
|
-
}
|
|
199
|
-
return parts.length > 0
|
|
200
|
-
? parts.join(", ")
|
|
201
|
-
: `Claude Code session in ${acc.projectName || "unknown project"}`;
|
|
202
|
-
}
|
|
203
|
-
export async function hookStop() {
|
|
204
|
-
// Read hook payload from stdin
|
|
205
|
-
let payload;
|
|
206
|
-
try {
|
|
207
|
-
const chunks = [];
|
|
208
|
-
for await (const chunk of process.stdin) {
|
|
209
|
-
chunks.push(chunk);
|
|
210
|
-
}
|
|
211
|
-
payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// No valid payload — nothing to do
|
|
215
|
-
process.exit(0);
|
|
216
|
-
}
|
|
217
|
-
const { session_id, transcript_path, cwd } = payload;
|
|
218
|
-
if (!session_id)
|
|
219
|
-
process.exit(0);
|
|
220
|
-
ensureDirs();
|
|
221
|
-
// Load or create session accumulator
|
|
222
|
-
let acc = readSessionAccumulator(session_id);
|
|
223
|
-
if (!acc) {
|
|
224
|
-
const gitInfo = extractGitInfo(cwd);
|
|
225
|
-
acc = {
|
|
226
|
-
sessionId: session_id,
|
|
227
|
-
cwd,
|
|
228
|
-
startedAt: new Date().toISOString(),
|
|
229
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
230
|
-
turns: 0,
|
|
231
|
-
toolCalls: [],
|
|
232
|
-
filesWritten: new Set(),
|
|
233
|
-
commitShas: new Set(),
|
|
234
|
-
gitBranch: gitInfo.branch,
|
|
235
|
-
gitRepo: gitInfo.repo,
|
|
236
|
-
projectName: gitInfo.projectName,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
// Analyze transcript to accumulate data
|
|
240
|
-
if (transcript_path) {
|
|
241
|
-
acc = analyzeTranscript(transcript_path, acc);
|
|
242
|
-
}
|
|
243
|
-
acc.lastUpdatedAt = new Date().toISOString();
|
|
244
|
-
// Save accumulator for future Stop events in this session
|
|
245
|
-
writeSessionAccumulator(acc);
|
|
246
|
-
// Flush to buffer if we have meaningful work
|
|
247
|
-
if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 3) {
|
|
248
|
-
flushToBuffer(acc);
|
|
249
|
-
// Auto-sync if config exists
|
|
250
|
-
const config = readConfig();
|
|
251
|
-
if (config?.apiKey) {
|
|
252
|
-
try {
|
|
253
|
-
const { syncEntries } = await import("./sync.js");
|
|
254
|
-
await syncEntries(config);
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
// Sync failure is non-fatal — entries stay in buffer
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
@@ -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,90 +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://www.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(`<!DOCTYPE html>
|
|
35
|
-
<html lang="en">
|
|
36
|
-
<head>
|
|
37
|
-
<meta charset="utf-8">
|
|
38
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39
|
-
<title>Basestream CLI — Authenticated</title>
|
|
40
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
41
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
42
|
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Newsreader:wght@500;600&display=swap" rel="stylesheet">
|
|
43
|
-
</head>
|
|
44
|
-
<body style="font-family: 'DM Sans', -apple-system, system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #faf8f4; color: #1a1714;">
|
|
45
|
-
<div style="text-align: center;">
|
|
46
|
-
<div style="display: flex; align-items: center; justify-content: center; gap: 0.625rem; margin-bottom: 2rem;">
|
|
47
|
-
<div style="display: flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; background: #1a1714; border-radius: 8px;">
|
|
48
|
-
<svg width="16" height="16" viewBox="0 0 22 22" fill="none"><path d="M7.39855 11.2475C10.1937 11.2476 12.4598 13.5124 12.4598 16.3052C12.4596 19.0979 10.1936 21.3616 7.39855 21.3618H0V16.3052C0 15.7395 0.094025 15.1955 0.265523 14.6876C0.304389 14.5726 0.345701 14.4588 0.392462 14.3476C0.602402 13.8482 0.890874 13.39 1.24144 12.9873C1.25986 12.9662 1.27857 12.9453 1.29734 12.9245C1.39051 12.821 1.48832 12.7218 1.58965 12.6263C1.60303 12.6137 1.61572 12.6004 1.62924 12.5879C1.73452 12.4908 1.8444 12.3987 1.95765 12.3107C1.97096 12.3004 1.98383 12.2895 1.99725 12.2793C2.1117 12.1922 2.23063 12.1108 2.35244 12.0336C2.36662 12.0246 2.3801 12.0145 2.39437 12.0056C2.44032 11.9771 2.48719 11.95 2.53412 11.9229C2.56226 11.9067 2.59065 11.8909 2.61913 11.8752C2.64893 11.8587 2.67864 11.8421 2.7088 11.8263C2.75348 11.8028 2.79844 11.7797 2.84389 11.7576C2.88103 11.7394 2.91922 11.7235 2.95686 11.7063C2.98716 11.6925 3.01707 11.6777 3.04769 11.6644C3.09301 11.6447 3.13912 11.6269 3.18511 11.6085C3.21662 11.5959 3.24764 11.582 3.27945 11.5701C3.32936 11.5513 3.38019 11.5349 3.43084 11.5177C3.45418 11.5097 3.47722 11.5008 3.50071 11.4932C3.55658 11.4751 3.61289 11.4581 3.66958 11.442C3.69166 11.4357 3.71376 11.4293 3.73596 11.4233C3.79907 11.4063 3.86289 11.3914 3.92695 11.3767C3.94209 11.3733 3.95718 11.3696 3.97237 11.3663C4.11504 11.335 4.25985 11.3095 4.40675 11.2906C4.43232 11.2873 4.45794 11.2842 4.48362 11.2812C4.54425 11.2744 4.60519 11.2685 4.66645 11.2638C4.67847 11.2629 4.69052 11.2623 4.70256 11.2614L4.80038 11.2545C4.88671 11.2501 4.97383 11.2475 5.06125 11.2475H7.39855ZM16.9388 11.2475C19.734 11.2476 22 13.5124 22 16.3052C21.9998 19.0979 19.7338 21.3616 16.9388 21.3618H13.2832C14.4337 20.2108 15.1709 18.4482 15.1709 16.4718C15.1709 14.2926 14.2749 12.3735 12.9163 11.2475H16.9388ZM7.39855 0.00116458C10.1937 0.0013425 12.4598 2.26497 12.4598 5.05775C12.4597 7.85045 10.1937 10.1142 7.39855 10.1143H0V5.05775C0 4.89236 0.00775963 4.7288 0.0232915 4.56747C0.0274579 4.52418 0.0331791 4.48116 0.038431 4.4382C0.0524815 4.32328 0.0692046 4.20938 0.0908369 4.09698L0.102483 4.03875C0.106227 4.02046 0.110188 4.00223 0.114128 3.98401C0.127164 3.92376 0.142054 3.8641 0.157218 3.80467C0.164069 3.77781 0.1709 3.75099 0.17818 3.72431C0.188513 3.68644 0.199603 3.64886 0.210788 3.61135C0.225482 3.56207 0.241227 3.51324 0.257371 3.46461C0.269763 3.42728 0.281404 3.38975 0.294638 3.35281C0.30559 3.32224 0.318053 3.29227 0.329575 3.26198C0.343044 3.22656 0.356098 3.19103 0.370335 3.156C0.387188 3.11454 0.404827 3.07346 0.422741 3.03256C0.44039 2.99225 0.458814 2.95235 0.477476 2.9126C0.491731 2.88224 0.505727 2.85179 0.520565 2.82177C0.537576 2.78734 0.555205 2.75326 0.572971 2.71928C0.592296 2.68233 0.610995 2.64506 0.6312 2.60865C0.657644 2.56099 0.68596 2.51444 0.713885 2.46774C0.725232 2.44876 0.736069 2.42949 0.747658 2.41067C0.773311 2.36902 0.800032 2.32807 0.826849 2.28723C0.843934 2.26121 0.860541 2.23488 0.87809 2.2092C0.953313 2.0991 1.03336 1.99256 1.11683 1.88894C1.18114 1.80912 1.24702 1.73059 1.31597 1.65486C1.47484 1.48036 1.64557 1.31664 1.82722 1.16574C1.85016 1.14669 1.87382 1.12849 1.89709 1.10984C1.99837 1.02867 2.10292 0.951415 2.21036 0.87809C2.23606 0.86056 2.26236 0.843915 2.28839 0.826849C2.32925 0.800052 2.37017 0.773292 2.41184 0.747658C2.43349 0.734341 2.45518 0.721061 2.47705 0.708062C2.52886 0.677257 2.58129 0.647377 2.63427 0.61839C2.64707 0.61139 2.65984 0.604319 2.6727 0.597427C2.72986 0.566794 2.78777 0.537396 2.84622 0.50892C2.86043 0.502002 2.87503 0.49591 2.88931 0.489122C2.94744 0.461478 3.00584 0.434234 3.06516 0.408766C3.0837 0.400809 3.10241 0.393216 3.12106 0.385475C3.17763 0.361989 3.23467 0.3394 3.29226 0.317929C3.30779 0.31214 3.32324 0.306101 3.33884 0.300461C3.39807 0.279043 3.45795 0.25915 3.51818 0.239903C3.54179 0.232361 3.56547 0.224979 3.58922 0.217776C3.63126 0.205025 3.67367 0.193353 3.71616 0.181674C3.7556 0.170838 3.79513 0.16013 3.83495 0.15023C3.9786 0.114503 4.12462 0.0848882 4.27283 0.0617225C4.28096 0.0604516 4.28914 0.059461 4.29728 0.0582288C4.36902 0.047376 4.44121 0.038089 4.5139 0.030279C4.5248 0.0291076 4.53558 0.0267227 4.5465 0.0256207C4.55542 0.0247215 4.56436 0.0241444 4.57329 0.0232915L4.57096 0.0244561C4.73223 0.00896687 4.89591 0.00117115 5.06125 0.00116458H7.39855ZM16.9388 0C19.734 0.000177881 22 2.26497 22 5.05775C21.9999 7.85045 19.7339 10.1142 16.9388 10.1143H12.9175C14.2754 8.98827 15.1708 7.06961 15.1709 4.89122C15.1709 2.91428 14.433 1.15104 13.282 0H16.9388Z" fill="#FAF8F4"/></svg>
|
|
49
|
-
</div>
|
|
50
|
-
<span style="font-size: 1rem; font-weight: 600;">basestream</span>
|
|
51
|
-
</div>
|
|
52
|
-
<div style="background: #fffdf9; border: 1px solid #e8e4de; border-radius: 16px; padding: 2rem; display: inline-block;">
|
|
53
|
-
<h1 style="font-family: 'Newsreader', Georgia, serif; font-size: 1.25rem; color: #6b7e6b; margin-bottom: 0.5rem;">✓ Authenticated</h1>
|
|
54
|
-
<p style="color: #8a8379; font-size: 0.875rem;">You can close this tab and return to your terminal.</p>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</body>
|
|
58
|
-
</html>`);
|
|
59
|
-
server.close();
|
|
60
|
-
writeConfig({
|
|
61
|
-
apiKey: key,
|
|
62
|
-
baseUrl,
|
|
63
|
-
orgId: orgId || undefined,
|
|
64
|
-
});
|
|
65
|
-
resolve(key);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
res.writeHead(400);
|
|
69
|
-
res.end("Missing key parameter");
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
server.listen(0, "127.0.0.1", () => {
|
|
73
|
-
const addr = server.address();
|
|
74
|
-
if (!addr || typeof addr === "string") {
|
|
75
|
-
reject(new Error("Failed to start callback server"));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const callbackPort = addr.port;
|
|
79
|
-
const authUrl = `${baseUrl}/api/auth/cli?callback=http://127.0.0.1:${callbackPort}`;
|
|
80
|
-
openBrowser(authUrl);
|
|
81
|
-
});
|
|
82
|
-
// Timeout after 2 minutes
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
server.close();
|
|
85
|
-
reject(new Error("Authentication timed out"));
|
|
86
|
-
}, 120_000);
|
|
87
|
-
});
|
|
88
|
-
check("Authenticated with Basestream");
|
|
89
|
-
return apiKey;
|
|
90
|
-
}
|
|
@@ -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
|
-
}
|
package/dist/cli/config.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare const BASESTREAM_DIR: string;
|
|
2
|
-
export declare const BUFFER_DIR: string;
|
|
3
|
-
export declare const CONFIG_FILE: string;
|
|
4
|
-
export declare const SESSION_DIR: string;
|
|
5
|
-
export interface BasestreamConfig {
|
|
6
|
-
apiKey: string;
|
|
7
|
-
baseUrl: string;
|
|
8
|
-
orgId?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function ensureDirs(): void;
|
|
11
|
-
export declare function readConfig(): BasestreamConfig | null;
|
|
12
|
-
export declare function writeConfig(config: BasestreamConfig): void;
|