@planckspace/cli 0.0.2 → 0.1.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 +109 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +7 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/scan.d.ts +5 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +51 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +31 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +77 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +40 -0
- package/dist/config.js.map +1 -0
- package/dist/correlate.d.ts +21 -0
- package/dist/correlate.d.ts.map +1 -0
- package/dist/correlate.js +204 -0
- package/dist/correlate.js.map +1 -0
- package/dist/db/store.d.ts +46 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +210 -0
- package/dist/db/store.js.map +1 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +7 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/scrapers/claudeCode.d.ts +4 -0
- package/dist/scrapers/claudeCode.d.ts.map +1 -0
- package/dist/scrapers/claudeCode.js +211 -0
- package/dist/scrapers/claudeCode.js.map +1 -0
- package/dist/scrapers/cursor.d.ts +9 -0
- package/dist/scrapers/cursor.d.ts.map +1 -0
- package/dist/scrapers/cursor.js +280 -0
- package/dist/scrapers/cursor.js.map +1 -0
- package/dist/scrapers/repo.d.ts +12 -0
- package/dist/scrapers/repo.d.ts.map +1 -0
- package/dist/scrapers/repo.js +40 -0
- package/dist/scrapers/repo.js.map +1 -0
- package/dist/scrapers/types.d.ts +36 -0
- package/dist/scrapers/types.d.ts.map +1 -0
- package/dist/scrapers/types.js +5 -0
- package/dist/scrapers/types.js.map +1 -0
- package/dist/scrapers/windsurf.d.ts +9 -0
- package/dist/scrapers/windsurf.d.ts.map +1 -0
- package/dist/scrapers/windsurf.js +255 -0
- package/dist/scrapers/windsurf.js.map +1 -0
- package/dist/sync/payload.d.ts +14 -0
- package/dist/sync/payload.d.ts.map +1 -0
- package/dist/sync/payload.js +36 -0
- package/dist/sync/payload.js.map +1 -0
- package/dist/sync/syncEngine.d.ts +16 -0
- package/dist/sync/syncEngine.d.ts.map +1 -0
- package/dist/sync/syncEngine.js +76 -0
- package/dist/sync/syncEngine.js.map +1 -0
- package/install.sh +126 -0
- package/package.json +39 -7
- package/index.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windsurf.d.ts","sourceRoot":"","sources":["../../src/scrapers/windsurf.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAQ,aAAa,EAAE,MAAM,YAAY,CAAC;AAwDtD,mFAAmF;AACnF,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAa1C;AAsJD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,MAAM,EAAsB,GAAG,aAAa,EAAE,CAkEvF"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { computeCostUsd } from "@planckspace/core";
|
|
6
|
+
import { resolveRepo } from "./repo.js";
|
|
7
|
+
// ── Status: BETA / UNVERIFIED (see docs/COVERAGE.md) ────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Windsurf (by Codeium) is a VS Code fork, so it uses the same per-user storage
|
|
10
|
+
// layout as VS Code / Cursor: <appSupport>/Windsurf/User/globalStorage/state.vscdb
|
|
11
|
+
// (an `ItemTable` key-value store), with its "Cascade" agent history kept there.
|
|
12
|
+
//
|
|
13
|
+
// IMPORTANT HONESTY NOTE: no Windsurf installation was available when this scraper
|
|
14
|
+
// was written, so its exact Cascade key names and message schema could NOT be
|
|
15
|
+
// verified against real data the way Cursor's were. This scraper therefore:
|
|
16
|
+
// • locates Windsurf's state DB across macOS / Linux / Windows (incl. "- Next"),
|
|
17
|
+
// • reads ONLY values under keys that plausibly hold Cascade conversations,
|
|
18
|
+
// • extracts only structurally-unambiguous fields (timestamps, message counts,
|
|
19
|
+
// and token/model fields *if present*),
|
|
20
|
+
// • and, crucially, does NOT fabricate sessions: if it finds the DB but cannot
|
|
21
|
+
// recognise a conversation schema, it warns once and returns nothing.
|
|
22
|
+
//
|
|
23
|
+
// On a machine with Windsurf this will either light up or print a clear "schema
|
|
24
|
+
// not recognised" diagnostic to drive the next iteration. We never invent tokens,
|
|
25
|
+
// costs, or models that the data does not contain.
|
|
26
|
+
// Keys under which a Windsurf/Cascade conversation might be stored. Kept narrow on
|
|
27
|
+
// purpose: matching loose patterns against every workbench value would manufacture
|
|
28
|
+
// junk sessions, which is worse than honestly reporting nothing.
|
|
29
|
+
const CASCADE_KEY_PATTERNS = [/cascade/i, /windsurf.*(chat|conversation|session)/i, /codeium.*(chat|conversation)/i];
|
|
30
|
+
// ── DB locations ─────────────────────────────────────────────────────────────────
|
|
31
|
+
/** Candidate Windsurf global-state DB paths for the current OS (stable + Next). */
|
|
32
|
+
export function windsurfDbPaths() {
|
|
33
|
+
const home = os.homedir();
|
|
34
|
+
const flavors = ["Windsurf", "Windsurf - Next"];
|
|
35
|
+
const bases = [];
|
|
36
|
+
if (process.platform === "darwin") {
|
|
37
|
+
for (const f of flavors)
|
|
38
|
+
bases.push(path.join(home, "Library", "Application Support", f));
|
|
39
|
+
}
|
|
40
|
+
else if (process.platform === "win32") {
|
|
41
|
+
const appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
42
|
+
for (const f of flavors)
|
|
43
|
+
bases.push(path.join(appData, f));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
for (const f of flavors)
|
|
47
|
+
bases.push(path.join(home, ".config", f));
|
|
48
|
+
}
|
|
49
|
+
return bases.map((b) => path.join(b, "User", "globalStorage", "state.vscdb"));
|
|
50
|
+
}
|
|
51
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────────
|
|
52
|
+
function parseJson(raw) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function toIso(v) {
|
|
61
|
+
if (v == null)
|
|
62
|
+
return null;
|
|
63
|
+
if (typeof v === "number") {
|
|
64
|
+
const d = new Date(v);
|
|
65
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
66
|
+
}
|
|
67
|
+
const d = new Date(v);
|
|
68
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
69
|
+
}
|
|
70
|
+
function messageTimestamp(m) {
|
|
71
|
+
return toIso(m.createdAt) ?? toIso(m.timestamp) ?? toIso(m.time);
|
|
72
|
+
}
|
|
73
|
+
function isUserMessage(m) {
|
|
74
|
+
const role = (m.role ?? m.sender ?? "").toString().toLowerCase();
|
|
75
|
+
if (role === "user" || role === "human")
|
|
76
|
+
return true;
|
|
77
|
+
if (m.type === 1 || m.type === "user")
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function messageTokens(m) {
|
|
82
|
+
const tc = m.tokenCount;
|
|
83
|
+
const u = m.usage;
|
|
84
|
+
return {
|
|
85
|
+
input: tc?.inputTokens ?? u?.inputTokens ?? u?.input_tokens ?? 0,
|
|
86
|
+
output: tc?.outputTokens ?? u?.outputTokens ?? u?.output_tokens ?? 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Recognise a value as a Cascade conversation and pull its message list out. */
|
|
90
|
+
function asConversation(value) {
|
|
91
|
+
if (!value || typeof value !== "object")
|
|
92
|
+
return null;
|
|
93
|
+
const obj = value;
|
|
94
|
+
const messages = obj.messages ?? obj.turns ?? obj.bubbles;
|
|
95
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
// Require at least one message to carry a recognisable timestamp; otherwise we
|
|
98
|
+
// cannot honestly place this session in time, so we decline rather than guess.
|
|
99
|
+
if (!messages.some((m) => messageTimestamp(m) !== null))
|
|
100
|
+
return null;
|
|
101
|
+
return { ...obj, messages };
|
|
102
|
+
}
|
|
103
|
+
function buildSession(conv, fallbackId) {
|
|
104
|
+
const messages = conv.messages ?? [];
|
|
105
|
+
const timestamps = messages
|
|
106
|
+
.map(messageTimestamp)
|
|
107
|
+
.filter((t) => t !== null)
|
|
108
|
+
.sort();
|
|
109
|
+
if (timestamps.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
let totalInput = 0;
|
|
112
|
+
let totalOutput = 0;
|
|
113
|
+
let totalCost = 0;
|
|
114
|
+
const turns = [];
|
|
115
|
+
let current = null;
|
|
116
|
+
let sessionModel = "unknown";
|
|
117
|
+
const close = () => {
|
|
118
|
+
if (current)
|
|
119
|
+
turns.push(current);
|
|
120
|
+
current = null;
|
|
121
|
+
};
|
|
122
|
+
for (const m of messages) {
|
|
123
|
+
const ts = messageTimestamp(m) ?? "";
|
|
124
|
+
const model = m.model ?? m.modelName ?? sessionModel;
|
|
125
|
+
if (model && model !== "unknown" && sessionModel === "unknown")
|
|
126
|
+
sessionModel = model;
|
|
127
|
+
if (isUserMessage(m)) {
|
|
128
|
+
close();
|
|
129
|
+
current = {
|
|
130
|
+
sequence: turns.length + 1,
|
|
131
|
+
model,
|
|
132
|
+
inputTokens: 0,
|
|
133
|
+
outputTokens: 0,
|
|
134
|
+
cacheReadTokens: 0,
|
|
135
|
+
cacheWriteTokens: 0,
|
|
136
|
+
costUsd: 0,
|
|
137
|
+
toolCalls: [],
|
|
138
|
+
filesTouched: [],
|
|
139
|
+
timestamp: ts,
|
|
140
|
+
};
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!current) {
|
|
144
|
+
current = {
|
|
145
|
+
sequence: turns.length + 1,
|
|
146
|
+
model,
|
|
147
|
+
inputTokens: 0,
|
|
148
|
+
outputTokens: 0,
|
|
149
|
+
cacheReadTokens: 0,
|
|
150
|
+
cacheWriteTokens: 0,
|
|
151
|
+
costUsd: 0,
|
|
152
|
+
toolCalls: [],
|
|
153
|
+
filesTouched: [],
|
|
154
|
+
timestamp: ts,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const { input, output } = messageTokens(m);
|
|
158
|
+
current.inputTokens += input;
|
|
159
|
+
current.outputTokens += output;
|
|
160
|
+
current.costUsd += computeCostUsd({ inputTokens: input, outputTokens: output, cacheReadTokens: 0, cacheWriteTokens: 0 }, model);
|
|
161
|
+
if (model && model !== "unknown")
|
|
162
|
+
current.model = model;
|
|
163
|
+
totalInput += input;
|
|
164
|
+
totalOutput += output;
|
|
165
|
+
}
|
|
166
|
+
close();
|
|
167
|
+
totalCost = turns.reduce((s, t) => s + t.costUsd, 0);
|
|
168
|
+
const workingDir = conv.workspacePath ?? conv.workspace ?? conv.cwd ?? "";
|
|
169
|
+
const { repoName, gitBranch } = resolveRepo(workingDir);
|
|
170
|
+
const id = conv.id ?? conv.conversationId ?? conv.sessionId ?? fallbackId;
|
|
171
|
+
return {
|
|
172
|
+
sessionId: `windsurf:${id}`,
|
|
173
|
+
tool: "windsurf",
|
|
174
|
+
model: sessionModel,
|
|
175
|
+
startedAt: timestamps[0],
|
|
176
|
+
endedAt: timestamps[timestamps.length - 1],
|
|
177
|
+
inputTokens: totalInput,
|
|
178
|
+
outputTokens: totalOutput,
|
|
179
|
+
cacheReadTokens: 0,
|
|
180
|
+
cacheWriteTokens: 0,
|
|
181
|
+
costUsd: totalCost,
|
|
182
|
+
turns,
|
|
183
|
+
workingDir,
|
|
184
|
+
turnCount: turns.length,
|
|
185
|
+
filesTouchedCount: 0, // not reliably derivable without a verified schema
|
|
186
|
+
repoName,
|
|
187
|
+
gitBranch,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// ── Main entrypoint ─────────────────────────────────────────────────────────────
|
|
191
|
+
/**
|
|
192
|
+
* Scan Windsurf's global state DB(s) for Cascade conversations.
|
|
193
|
+
* @param dbPaths override the DB locations (used by tests). Defaults to OS paths.
|
|
194
|
+
*/
|
|
195
|
+
export function scanWindsurfLogs(dbPaths = windsurfDbPaths()) {
|
|
196
|
+
const existing = dbPaths.filter((p) => fs.existsSync(p));
|
|
197
|
+
if (existing.length === 0)
|
|
198
|
+
return []; // Windsurf not installed — expected, silent.
|
|
199
|
+
const sessions = [];
|
|
200
|
+
for (const dbPath of existing) {
|
|
201
|
+
let db;
|
|
202
|
+
try {
|
|
203
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
console.warn(`[windsurf] Cannot open ${dbPath}: ${String(err)} — skipping`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((t) => t.name);
|
|
211
|
+
let matchedKeys = 0;
|
|
212
|
+
let recognised = 0;
|
|
213
|
+
for (const table of tables) {
|
|
214
|
+
// Every VS Code KV table we know of is (key TEXT, value BLOB/TEXT).
|
|
215
|
+
const cols = db.prepare(`PRAGMA table_info("${table}")`).all().map((c) => c.name);
|
|
216
|
+
if (!cols.includes("key") || !cols.includes("value"))
|
|
217
|
+
continue;
|
|
218
|
+
const rows = db.prepare(`SELECT key, value FROM "${table}"`).all();
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
if (!CASCADE_KEY_PATTERNS.some((re) => re.test(row.key)))
|
|
221
|
+
continue;
|
|
222
|
+
matchedKeys++;
|
|
223
|
+
const parsed = parseJson(row.value.toString());
|
|
224
|
+
if (!parsed)
|
|
225
|
+
continue;
|
|
226
|
+
// A value may be a single conversation or an array of them.
|
|
227
|
+
const candidates = Array.isArray(parsed) ? parsed : [parsed];
|
|
228
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
229
|
+
const conv = asConversation(candidates[i]);
|
|
230
|
+
if (!conv)
|
|
231
|
+
continue;
|
|
232
|
+
const session = buildSession(conv, `${row.key}:${i}`);
|
|
233
|
+
if (session) {
|
|
234
|
+
sessions.push(session);
|
|
235
|
+
recognised++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (matchedKeys > 0 && recognised === 0) {
|
|
241
|
+
console.warn(`[windsurf] Found ${matchedKeys} Cascade-like key(s) in ${dbPath} but none matched a ` +
|
|
242
|
+
`recognised conversation schema. Windsurf support is beta/unverified — please open an ` +
|
|
243
|
+
`issue so we can map its format. No sessions were fabricated.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
console.warn(`[windsurf] Failed to read ${dbPath}: ${String(err)}`);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
db.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return sessions;
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=windsurf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windsurf.js","sourceRoot":"","sources":["../../src/scrapers/windsurf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TelemetryPayload } from "@planckspace/core";
|
|
2
|
+
import type { SessionRow } from "../db/store.js";
|
|
3
|
+
/** The shape returned by getUnsyncedSessions() — SessionRow with turnsJson stripped. */
|
|
4
|
+
export type UnsyncedSession = Omit<SessionRow, "turnsJson">;
|
|
5
|
+
/**
|
|
6
|
+
* Build the wire payload for a single session — METADATA ONLY.
|
|
7
|
+
*
|
|
8
|
+
* PRIVACY INVARIANT: this function maps each TelemetryPayload field EXPLICITLY.
|
|
9
|
+
* It never spreads the row and never references turnsJson, prompt text, response
|
|
10
|
+
* text, or file contents. Anything not listed below is, by construction, not sent.
|
|
11
|
+
* The privacy guard test (payload.privacy.test.ts) enforces this and must always pass.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildTelemetryPayload(row: UnsyncedSession, workspaceId: string): TelemetryPayload;
|
|
14
|
+
//# sourceMappingURL=payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/sync/payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,MAAM,GAClB,gBAAgB,CA0BlB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the wire payload for a single session — METADATA ONLY.
|
|
3
|
+
*
|
|
4
|
+
* PRIVACY INVARIANT: this function maps each TelemetryPayload field EXPLICITLY.
|
|
5
|
+
* It never spreads the row and never references turnsJson, prompt text, response
|
|
6
|
+
* text, or file contents. Anything not listed below is, by construction, not sent.
|
|
7
|
+
* The privacy guard test (payload.privacy.test.ts) enforces this and must always pass.
|
|
8
|
+
*/
|
|
9
|
+
export function buildTelemetryPayload(row, workspaceId) {
|
|
10
|
+
return {
|
|
11
|
+
sessionId: row.id,
|
|
12
|
+
// The DB only ever holds scraper-sourced Claude Code sessions today; the cast
|
|
13
|
+
// narrows the stored string to the core enum without widening the wire contract.
|
|
14
|
+
tool: row.tool,
|
|
15
|
+
model: row.model,
|
|
16
|
+
startedAt: row.startedAt,
|
|
17
|
+
endedAt: row.endedAt,
|
|
18
|
+
inputTokens: row.inputTokens,
|
|
19
|
+
outputTokens: row.outputTokens,
|
|
20
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
21
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
22
|
+
costUsd: row.costUsd,
|
|
23
|
+
// Backend schema requires a non-empty repoName.
|
|
24
|
+
repoName: row.repoName ?? "unknown",
|
|
25
|
+
gitBranch: row.gitBranch,
|
|
26
|
+
// Recorded by git correlation (the repo's `user.email`), not from prompt
|
|
27
|
+
// content. May differ from the login email — the cloud maps it to a member.
|
|
28
|
+
gitAuthorEmail: row.gitAuthorEmail,
|
|
29
|
+
outcome: row.outcome,
|
|
30
|
+
turnCount: row.turnCount,
|
|
31
|
+
filesTouchedCount: row.filesTouchedCount,
|
|
32
|
+
source: "scraper",
|
|
33
|
+
workspaceId,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/sync/payload.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAoB,EACpB,WAAmB;IAEnB,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,EAAE;QACjB,8EAA8E;QAC9E,iFAAiF;QACjF,IAAI,EAAE,GAAG,CAAC,IAAgC;QAC1C,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;QACtC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,gDAAgD;QAChD,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,SAAS;QACnC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,yEAAyE;QACzE,4EAA4E;QAC5E,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,OAAO,EAAE,GAAG,CAAC,OAAsC;QACnD,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,MAAM,EAAE,SAAS;QACjB,WAAW;KACZ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SyncDeps = {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
};
|
|
6
|
+
export type SyncResult = {
|
|
7
|
+
synced: number;
|
|
8
|
+
failed: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Push all unsynced sessions to {apiUrl}/api/ingest in batches of up to 100.
|
|
12
|
+
* Successful batches are marked synced; failed batches record lastError and stay
|
|
13
|
+
* unsynced for the next run. Transient failures retry with exponential backoff.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncOnce(deps: SyncDeps): Promise<SyncResult>;
|
|
16
|
+
//# sourceMappingURL=syncEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../src/sync/syncEngine.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAkD5D;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CA+BlE"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getUnsyncedSessions, markSessionsSynced, recordSyncError, } from "../db/store.js";
|
|
2
|
+
import { buildTelemetryPayload } from "./payload.js";
|
|
3
|
+
const BATCH_SIZE = 100;
|
|
4
|
+
const MAX_RETRIES = 3;
|
|
5
|
+
function chunk(arr, size) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (let i = 0; i < arr.length; i += size)
|
|
8
|
+
out.push(arr.slice(i, i + size));
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
/** A failure worth retrying: no response (network) or a 5xx / 429 from the server. */
|
|
15
|
+
function isTransient(outcome) {
|
|
16
|
+
if (outcome.status === undefined)
|
|
17
|
+
return true;
|
|
18
|
+
return outcome.status >= 500 || outcome.status === 429;
|
|
19
|
+
}
|
|
20
|
+
async function postBatch(deps, payloads) {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${deps.apiUrl}/api/ingest`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
Authorization: `Bearer ${deps.token}`,
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(payloads),
|
|
29
|
+
});
|
|
30
|
+
if (res.ok)
|
|
31
|
+
return { ok: true, status: res.status };
|
|
32
|
+
const text = await res.text().catch(() => "");
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
status: res.status,
|
|
36
|
+
error: `HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { ok: false, error: `network error: ${String(err)}` };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Push all unsynced sessions to {apiUrl}/api/ingest in batches of up to 100.
|
|
45
|
+
* Successful batches are marked synced; failed batches record lastError and stay
|
|
46
|
+
* unsynced for the next run. Transient failures retry with exponential backoff.
|
|
47
|
+
*/
|
|
48
|
+
export async function syncOnce(deps) {
|
|
49
|
+
const rows = getUnsyncedSessions();
|
|
50
|
+
if (rows.length === 0)
|
|
51
|
+
return { synced: 0, failed: 0 };
|
|
52
|
+
let synced = 0;
|
|
53
|
+
let failed = 0;
|
|
54
|
+
for (const batch of chunk(rows, BATCH_SIZE)) {
|
|
55
|
+
const ids = batch.map((r) => r.id);
|
|
56
|
+
const payloads = batch.map((r) => buildTelemetryPayload(r, deps.workspaceId));
|
|
57
|
+
let outcome = { ok: false };
|
|
58
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
59
|
+
outcome = await postBatch(deps, payloads);
|
|
60
|
+
if (outcome.ok || !isTransient(outcome) || attempt === MAX_RETRIES)
|
|
61
|
+
break;
|
|
62
|
+
// Exponential backoff: 500ms, 1s, 2s.
|
|
63
|
+
await sleep(500 * 2 ** attempt);
|
|
64
|
+
}
|
|
65
|
+
if (outcome.ok) {
|
|
66
|
+
markSessionsSynced(ids);
|
|
67
|
+
synced += ids.length;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
recordSyncError(ids, outcome.error ?? "unknown error");
|
|
71
|
+
failed += ids.length;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { synced, failed };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=syncEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncEngine.js","sourceRoot":"","sources":["../../src/sync/syncEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,MAAM,WAAW,GAAG,CAAC,CAAC;AAUtB,SAAS,KAAK,CAAI,GAAQ,EAAE,IAAY;IACtC,MAAM,GAAG,GAAU,EAAE,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC5E,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AASD,sFAAsF;AACtF,SAAS,WAAW,CAAC,OAAoB;IACvC,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,OAAO,CAAC,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,CAAC;AACzD,CAAC;AAED,KAAK,UAAU,SAAS,CACtB,IAAc,EACd,QAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,aAAa,EAAE;YACnD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE;aACtC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;SAC/B,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,KAAK,EAAE,QAAQ,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;SACpE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAc;IAC3C,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAEvD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAC/B,qBAAqB,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAC3C,CAAC;QAEF,IAAI,OAAO,GAAgB,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QACzC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YACxD,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC1C,IAAI,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,WAAW;gBAAE,MAAM;YAC1E,sCAAsC;YACtC,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;YACf,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACxB,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,IAAI,eAAe,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC"}
|
package/install.sh
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PlanckSpace installer — https://planckspace.dev/install
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# curl -fsSL https://planckspace.dev/install | sh
|
|
6
|
+
# curl -fsSL https://planckspace.dev/install | sh -s -- --token pk_live_xxx
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
DASHBOARD_URL="https://app.planckspace.dev"
|
|
10
|
+
VSCODE_EXT_URL="https://marketplace.visualstudio.com/items?itemName=planckspace.planckspace"
|
|
11
|
+
PKG="@planckspace/cli"
|
|
12
|
+
|
|
13
|
+
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
say() { printf '\n%s\n' "$*"; }
|
|
15
|
+
info() { printf ' %s\n' "$*"; }
|
|
16
|
+
die() { printf '\nERROR: %s\n\n' "$*" >&2; exit 1; }
|
|
17
|
+
|
|
18
|
+
# ── parse args ───────────────────────────────────────────────────────────────
|
|
19
|
+
TOKEN=""
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--token) TOKEN="${2:-}"; shift 2 ;;
|
|
23
|
+
--token=*) TOKEN="${1#--token=}"; shift ;;
|
|
24
|
+
*) shift ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
# ── OS / arch check ──────────────────────────────────────────────────────────
|
|
29
|
+
OS="$(uname -s 2>/dev/null || echo unknown)"
|
|
30
|
+
ARCH="$(uname -m 2>/dev/null || echo unknown)"
|
|
31
|
+
|
|
32
|
+
case "$OS" in
|
|
33
|
+
Darwin|Linux) ;;
|
|
34
|
+
MINGW*|MSYS*|CYGWIN*|Windows_NT)
|
|
35
|
+
say "Windows detected."
|
|
36
|
+
say "PlanckSpace requires WSL (Windows Subsystem for Linux)."
|
|
37
|
+
info "1. Open PowerShell as Administrator and run: wsl --install"
|
|
38
|
+
info "2. After WSL is set up, open a WSL terminal and run:"
|
|
39
|
+
info " curl -fsSL https://planckspace.dev/install | sh"
|
|
40
|
+
exit 1
|
|
41
|
+
;;
|
|
42
|
+
*)
|
|
43
|
+
die "Unsupported OS: $OS. Install manually with: npm install -g $PKG"
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
say "PlanckSpace installer (OS: $OS / arch: $ARCH)"
|
|
48
|
+
|
|
49
|
+
# ── Node.js check ────────────────────────────────────────────────────────────
|
|
50
|
+
node_ok() {
|
|
51
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
52
|
+
local v
|
|
53
|
+
v="$(node --version 2>/dev/null | sed 's/v//' | cut -d. -f1)"
|
|
54
|
+
[[ "${v:-0}" -ge 20 ]]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ! node_ok; then
|
|
58
|
+
say "Node.js 20+ is required but was not found."
|
|
59
|
+
say "Install it with nvm (recommended — no sudo needed):"
|
|
60
|
+
info "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash"
|
|
61
|
+
info "source \"\${HOME}/.nvm/nvm.sh\""
|
|
62
|
+
info "nvm install 20"
|
|
63
|
+
say "Or download from: https://nodejs.org/en/download"
|
|
64
|
+
say "Then re-run this installer."
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
NODE_VER="$(node --version)"
|
|
69
|
+
info "Node $NODE_VER found."
|
|
70
|
+
|
|
71
|
+
# ── npm check ────────────────────────────────────────────────────────────────
|
|
72
|
+
command -v npm >/dev/null 2>&1 || die "npm not found. Re-install Node.js from https://nodejs.org"
|
|
73
|
+
|
|
74
|
+
# ── install CLI ──────────────────────────────────────────────────────────────
|
|
75
|
+
say "Installing $PKG..."
|
|
76
|
+
npm install -g "$PKG" --loglevel error
|
|
77
|
+
|
|
78
|
+
# verify the binary is reachable
|
|
79
|
+
command -v planck >/dev/null 2>&1 || {
|
|
80
|
+
# npm global bin might not be in PATH — try to fix
|
|
81
|
+
NPM_BIN="$(npm bin -g 2>/dev/null || true)"
|
|
82
|
+
if [[ -n "$NPM_BIN" && -x "$NPM_BIN/planck" ]]; then
|
|
83
|
+
export PATH="$NPM_BIN:$PATH"
|
|
84
|
+
else
|
|
85
|
+
die "'planck' binary not found after install. Add \`$(npm prefix -g)/bin\` to your PATH."
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── init ─────────────────────────────────────────────────────────────────────
|
|
90
|
+
say "Initializing ~/.planckspace..."
|
|
91
|
+
planck init
|
|
92
|
+
|
|
93
|
+
# ── login ────────────────────────────────────────────────────────────────────
|
|
94
|
+
CONNECTED=false
|
|
95
|
+
if [[ -n "$TOKEN" ]]; then
|
|
96
|
+
say "Connecting to your workspace..."
|
|
97
|
+
planck login --token "$TOKEN" && CONNECTED=true || true
|
|
98
|
+
else
|
|
99
|
+
say "No token provided."
|
|
100
|
+
info "Visit $DASHBOARD_URL to get your invite token, then run:"
|
|
101
|
+
info " planck login <your-token>"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# ── scan (backfill existing history) ─────────────────────────────────────────
|
|
105
|
+
say "Scanning local AI session history (this is fast)..."
|
|
106
|
+
planck scan
|
|
107
|
+
|
|
108
|
+
# ── sync (only when connected) ───────────────────────────────────────────────
|
|
109
|
+
if [[ "$CONNECTED" == "true" ]]; then
|
|
110
|
+
say "Syncing data to your workspace..."
|
|
111
|
+
planck sync
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ── done ─────────────────────────────────────────────────────────────────────
|
|
115
|
+
say "--------------------------------------"
|
|
116
|
+
say "PlanckSpace is ready."
|
|
117
|
+
say "--------------------------------------"
|
|
118
|
+
if [[ "$CONNECTED" == "true" ]]; then
|
|
119
|
+
info "Dashboard: $DASHBOARD_URL"
|
|
120
|
+
else
|
|
121
|
+
info "Next step: planck login <token>"
|
|
122
|
+
info "Dashboard: $DASHBOARD_URL"
|
|
123
|
+
fi
|
|
124
|
+
info "VS Code extension: $VSCODE_EXT_URL"
|
|
125
|
+
info "Check setup: planck status"
|
|
126
|
+
say ""
|
package/package.json
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planckspace/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PlanckSpace CLI — sync AI token usage from your editor to the team ledger",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"planck": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"clean": "rm -rf dist",
|
|
13
|
+
"scan:test": "tsx src/scripts/scanTest.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"release": "npm publish"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"install.sh"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@planckspace/core": "^0.1.0",
|
|
30
|
+
"better-sqlite3": "^12.10.0",
|
|
31
|
+
"commander": "^15.0.0",
|
|
32
|
+
"dotenv": "^17.4.2",
|
|
33
|
+
"simple-git": "^3.36.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
37
|
+
"@types/node": "^25.9.1",
|
|
38
|
+
"tsx": "^4.22.4",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.7"
|
|
41
|
+
}
|
|
10
42
|
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// placeholder
|