@overscore/cli 0.11.0 → 0.11.2
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/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ function parseEnv(content) {
|
|
|
23
23
|
}
|
|
24
24
|
return env;
|
|
25
25
|
}
|
|
26
|
-
function loadEnv() {
|
|
26
|
+
async function loadEnv() {
|
|
27
27
|
// (1) Local .env (inside a dashboard project)
|
|
28
28
|
const envPath = path.resolve(process.cwd(), ".env");
|
|
29
29
|
if (fs.existsSync(envPath)) {
|
|
@@ -49,33 +49,32 @@ function loadEnv() {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
// No creds
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
// No creds — trigger browser auth inline, then re-check
|
|
53
|
+
console.log("\n No Overscore credentials found. Launching browser login...\n");
|
|
54
|
+
await authLogin();
|
|
55
|
+
const configPath2 = path.join(process.env.HOME || "", ".overscore", "config");
|
|
56
|
+
if (fs.existsSync(configPath2)) {
|
|
57
|
+
const cfg2 = parseEnv(fs.readFileSync(configPath2, "utf-8"));
|
|
58
|
+
if (cfg2.OVERSCORE_API_KEY) {
|
|
59
|
+
return {
|
|
60
|
+
apiUrl: cfg2.OVERSCORE_API_URL || `${HUB_URL}/api`,
|
|
61
|
+
apiKey: cfg2.OVERSCORE_API_KEY,
|
|
62
|
+
dashboardSlug: path.basename(process.cwd()),
|
|
63
|
+
projectSlug: cfg2.OVERSCORE_PROJECT_SLUG || "",
|
|
64
|
+
};
|
|
63
65
|
}
|
|
64
|
-
console.error("\n Error: No credentials found in this analysis folder.");
|
|
65
|
-
console.error(" This usually means the analysis hasn't been linked to your account yet.");
|
|
66
|
-
console.error(" Run:");
|
|
67
|
-
console.error(` npx @overscore/cli analysis pull ${analysisSlug || "<slug>"}`);
|
|
68
|
-
console.error(" to refresh credentials and link this folder, or:");
|
|
69
|
-
console.error(" npx @overscore/cli auth login");
|
|
70
|
-
console.error(" if you've never set up the CLI on this machine.\n");
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
// Logged in but no project API key yet — the user needs to run setup first
|
|
68
|
+
console.log(`
|
|
69
|
+
Logged in! To get your first project set up:
|
|
70
|
+
|
|
71
|
+
Create a dashboard: npx create-overscore --dashboard <name> --project <slug>
|
|
72
|
+
Pull an analysis: npx @overscore/cli analysis pull <slug>
|
|
73
|
+
`);
|
|
74
|
+
process.exit(0);
|
|
76
75
|
}
|
|
77
76
|
async function apiRequest(method, urlPath, body, command) {
|
|
78
|
-
const { apiUrl, apiKey } = loadEnv();
|
|
77
|
+
const { apiUrl, apiKey } = await loadEnv();
|
|
79
78
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
80
79
|
const url = `${baseUrl}${urlPath}`;
|
|
81
80
|
const headers = {
|
|
@@ -195,7 +194,7 @@ function extractZip(zipBuffer, targetDir) {
|
|
|
195
194
|
}
|
|
196
195
|
// ── Commands ────────────────────────────────────────────────────────
|
|
197
196
|
async function deploy() {
|
|
198
|
-
const { apiUrl, apiKey, dashboardSlug } = loadEnv();
|
|
197
|
+
const { apiUrl, apiKey, dashboardSlug } = await loadEnv();
|
|
199
198
|
// Parse --message flag
|
|
200
199
|
const msgIndex = process.argv.indexOf("--message");
|
|
201
200
|
const commitMessage = msgIndex !== -1 && process.argv[msgIndex + 1]
|
|
@@ -264,31 +263,24 @@ async function deploy() {
|
|
|
264
263
|
URL: ${data.url}
|
|
265
264
|
`);
|
|
266
265
|
// Sync context files to Hub
|
|
267
|
-
const { projectSlug } = loadEnv();
|
|
266
|
+
const { projectSlug } = await loadEnv();
|
|
268
267
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
269
268
|
// Upload knowledge base (project-level, shared across all artifacts)
|
|
270
269
|
await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
|
|
271
|
-
// Sync
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (storedHash && hubHash && storedHash !== hubHash) {
|
|
286
|
-
console.log(" ⚠ Company context was updated in the Hub since you pulled.");
|
|
287
|
-
console.log(` Review at: ${baseUrl}/projects/${projectSlug}/context`);
|
|
288
|
-
console.log(" Your local changes were NOT uploaded to avoid overwriting.");
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
const putRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, {
|
|
270
|
+
// Sync dashboard context — skip stub files (they have no real content)
|
|
271
|
+
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
|
|
272
|
+
const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
|
|
273
|
+
const activeDashCtxPath = fs.existsSync(dashCtxPath)
|
|
274
|
+
? dashCtxPath
|
|
275
|
+
: fs.existsSync(legacyDashCtxPath)
|
|
276
|
+
? legacyDashCtxPath
|
|
277
|
+
: null;
|
|
278
|
+
if (activeDashCtxPath) {
|
|
279
|
+
const content = fs.readFileSync(activeDashCtxPath, "utf-8").trim();
|
|
280
|
+
const isStub = !content || content.startsWith("<!--") || content === `# ${dashboardSlug}`;
|
|
281
|
+
if (!isStub) {
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
|
|
292
284
|
method: "PUT",
|
|
293
285
|
headers: {
|
|
294
286
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -296,47 +288,24 @@ async function deploy() {
|
|
|
296
288
|
},
|
|
297
289
|
body: JSON.stringify({ context_md: content }),
|
|
298
290
|
});
|
|
299
|
-
if (
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (putData.context_md_hash) {
|
|
303
|
-
fs.mkdirSync(path.dirname(hashPath), { recursive: true });
|
|
304
|
-
fs.writeFileSync(hashPath, putData.context_md_hash);
|
|
305
|
-
}
|
|
306
|
-
console.log(" Company context synced.");
|
|
291
|
+
if (res.ok) {
|
|
292
|
+
const lines = content.split("\n").length;
|
|
293
|
+
console.log(` Dashboard context synced (${lines} lines).`);
|
|
307
294
|
}
|
|
308
295
|
}
|
|
296
|
+
catch {
|
|
297
|
+
// Non-fatal
|
|
298
|
+
}
|
|
309
299
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Sync dashboard context (no conflict detection needed — per-dashboard)
|
|
315
|
-
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
|
|
316
|
-
const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
|
|
317
|
-
const activeDashCtxPath = fs.existsSync(dashCtxPath) ? dashCtxPath : fs.existsSync(legacyDashCtxPath) ? legacyDashCtxPath : null;
|
|
318
|
-
if (activeDashCtxPath) {
|
|
319
|
-
const content = fs.readFileSync(activeDashCtxPath, "utf-8");
|
|
320
|
-
try {
|
|
321
|
-
await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
|
|
322
|
-
method: "PUT",
|
|
323
|
-
headers: {
|
|
324
|
-
Authorization: `Bearer ${apiKey}`,
|
|
325
|
-
"Content-Type": "application/json",
|
|
326
|
-
},
|
|
327
|
-
body: JSON.stringify({ context_md: content }),
|
|
328
|
-
});
|
|
329
|
-
console.log(" Dashboard context synced.");
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
// Non-fatal
|
|
300
|
+
else {
|
|
301
|
+
console.log(" Dashboard context: stub, not synced — run save-knowledge to populate it.");
|
|
333
302
|
}
|
|
334
303
|
}
|
|
335
304
|
// Pull latest platform rules so the local project stays up to date
|
|
336
305
|
await syncPlatformRules(process.cwd(), baseUrl, apiKey);
|
|
337
306
|
}
|
|
338
307
|
async function queryList() {
|
|
339
|
-
const { dashboardSlug } = loadEnv();
|
|
308
|
+
const { dashboardSlug } = await loadEnv();
|
|
340
309
|
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
|
|
341
310
|
console.log(`\n Dashboard: ${data.dashboard}\n`);
|
|
342
311
|
// Registered queries
|
|
@@ -383,7 +352,7 @@ async function queryShow(name) {
|
|
|
383
352
|
console.error("\n Usage: npx @overscore/cli query show <name>\n");
|
|
384
353
|
process.exit(1);
|
|
385
354
|
}
|
|
386
|
-
const { dashboardSlug } = loadEnv();
|
|
355
|
+
const { dashboardSlug } = await loadEnv();
|
|
387
356
|
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
|
|
388
357
|
const query = data.queries.find((q) => q.query_name === name);
|
|
389
358
|
if (!query) {
|
|
@@ -451,7 +420,7 @@ async function queryAdd(name, sql) {
|
|
|
451
420
|
console.error("\n Error: Query name must start with a lowercase letter and contain only lowercase letters, numbers, and underscores.\n");
|
|
452
421
|
process.exit(1);
|
|
453
422
|
}
|
|
454
|
-
const { dashboardSlug } = loadEnv();
|
|
423
|
+
const { dashboardSlug } = await loadEnv();
|
|
455
424
|
let body;
|
|
456
425
|
if (datasetName) {
|
|
457
426
|
// Look up the dataset UUID from project_datasets in the queries endpoint
|
|
@@ -484,7 +453,7 @@ async function queryUpdate(name, sql) {
|
|
|
484
453
|
console.error(" npx @overscore/cli query update <name> --file query.sql\n");
|
|
485
454
|
process.exit(1);
|
|
486
455
|
}
|
|
487
|
-
const { dashboardSlug } = loadEnv();
|
|
456
|
+
const { dashboardSlug } = await loadEnv();
|
|
488
457
|
const data = (await apiRequest("PUT", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`, { sql_text: resolvedSql }));
|
|
489
458
|
console.log(`\n Query "${data.query_name}" updated.`);
|
|
490
459
|
console.log(" Cache has been invalidated — fresh data on next load.\n");
|
|
@@ -499,7 +468,7 @@ async function queryRemove(name) {
|
|
|
499
468
|
console.log(" Cancelled.\n");
|
|
500
469
|
return;
|
|
501
470
|
}
|
|
502
|
-
const { dashboardSlug } = loadEnv();
|
|
471
|
+
const { dashboardSlug } = await loadEnv();
|
|
503
472
|
await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
|
|
504
473
|
console.log(`\n Query "${name}" deleted.\n`);
|
|
505
474
|
}
|
|
@@ -557,7 +526,7 @@ async function queryTest(sql) {
|
|
|
557
526
|
console.error(" npx @overscore/cli query run --file queries/foo.sql\n");
|
|
558
527
|
process.exit(1);
|
|
559
528
|
}
|
|
560
|
-
const { projectSlug } = loadEnv();
|
|
529
|
+
const { projectSlug } = await loadEnv();
|
|
561
530
|
if (!projectSlug) {
|
|
562
531
|
const analysisMetaPath = path.resolve(process.cwd(), ".overscore", "analysis.json");
|
|
563
532
|
if (fs.existsSync(analysisMetaPath)) {
|
|
@@ -736,51 +705,85 @@ VITE_OVERSCORE_DASHBOARD_SLUG=${dashboardSlug}
|
|
|
736
705
|
VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
737
706
|
`;
|
|
738
707
|
fs.writeFileSync(path.join(targetDir, ".env"), envContent, { mode: 0o600 });
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
708
|
+
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
709
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
710
|
+
// Sync project knowledge base (preferred) — or fall back to legacy company-context.md
|
|
711
|
+
const hasKnowledge = await syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey);
|
|
712
|
+
if (!hasKnowledge) {
|
|
713
|
+
try {
|
|
714
|
+
const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, {
|
|
715
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
716
|
+
});
|
|
717
|
+
if (ctxRes.ok) {
|
|
718
|
+
const ctxData = (await ctxRes.json());
|
|
719
|
+
if (ctxData.context_md) {
|
|
720
|
+
fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
|
|
721
|
+
if (ctxData.context_md_hash) {
|
|
722
|
+
fs.writeFileSync(path.join(rulesDir, ".context-hash"), ctxData.context_md_hash);
|
|
723
|
+
}
|
|
753
724
|
}
|
|
754
|
-
console.log(" Updated company context from Hub.");
|
|
755
725
|
}
|
|
756
726
|
}
|
|
727
|
+
catch {
|
|
728
|
+
// Non-fatal
|
|
729
|
+
}
|
|
757
730
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
// Pull dashboard context from Hub (may be newer than the source zip)
|
|
731
|
+
// Pull dashboard context from Hub
|
|
732
|
+
let dashboardContextLoaded = false;
|
|
762
733
|
try {
|
|
763
734
|
const dashCtxRes = await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
|
|
764
735
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
765
736
|
});
|
|
766
737
|
if (dashCtxRes.ok) {
|
|
767
738
|
const dashCtxData = (await dashCtxRes.json());
|
|
768
|
-
if (dashCtxData.context_md) {
|
|
769
|
-
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
770
|
-
fs.mkdirSync(rulesDir, { recursive: true });
|
|
739
|
+
if (dashCtxData.context_md && dashCtxData.context_md.trim()) {
|
|
771
740
|
fs.writeFileSync(path.join(rulesDir, "this-dashboard.md"), dashCtxData.context_md);
|
|
772
|
-
|
|
741
|
+
dashboardContextLoaded = true;
|
|
773
742
|
}
|
|
774
743
|
}
|
|
775
744
|
}
|
|
776
745
|
catch {
|
|
777
746
|
// Non-fatal
|
|
778
747
|
}
|
|
779
|
-
//
|
|
748
|
+
// Create a stub this-dashboard.md if none came from the Hub so Claude knows to fill it in
|
|
749
|
+
if (!dashboardContextLoaded) {
|
|
750
|
+
const stubPath = path.join(rulesDir, "this-dashboard.md");
|
|
751
|
+
if (!fs.existsSync(stubPath) || fs.readFileSync(stubPath, "utf-8").trim() === "") {
|
|
752
|
+
fs.writeFileSync(stubPath, `# ${slug}\n\n<!-- Claude: fill this in as you build. What does this dashboard answer? Who is the audience? Which queries power which charts? Any non-obvious filter logic or design decisions? -->\n`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Pull latest platform rules (includes save-knowledge skill)
|
|
780
756
|
await syncPlatformRules(targetDir, baseUrl, apiKey);
|
|
757
|
+
// Print context summary so the developer knows what Claude has access to
|
|
758
|
+
const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
|
|
759
|
+
const knowledgeFiles = fs.existsSync(knowledgeDir)
|
|
760
|
+
? fs.readdirSync(knowledgeDir).filter((f) => f.endsWith(".md") && f !== "INDEX.md")
|
|
761
|
+
: [];
|
|
762
|
+
const dashCtxPath = path.join(rulesDir, "this-dashboard.md");
|
|
763
|
+
const dashCtxContent = fs.existsSync(dashCtxPath) ? fs.readFileSync(dashCtxPath, "utf-8").trim() : "";
|
|
764
|
+
const dashCtxIsStub = dashCtxContent.startsWith("<!--") || dashCtxContent === `# ${slug}`;
|
|
765
|
+
const sqlStylePath = path.join(rulesDir, "sql-style.md");
|
|
781
766
|
console.log(`
|
|
782
767
|
Pulled v${version}${commitMsg ? ` — "${commitMsg}"` : ""}
|
|
783
768
|
|
|
769
|
+
Context loaded:`);
|
|
770
|
+
if (knowledgeFiles.length > 0) {
|
|
771
|
+
console.log(` Knowledge base: ${knowledgeFiles.length} file${knowledgeFiles.length !== 1 ? "s" : ""} (${knowledgeFiles.join(", ")})`);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
console.log(` Knowledge base: empty — Claude will build it as you work`);
|
|
775
|
+
}
|
|
776
|
+
if (dashCtxIsStub || !dashCtxContent) {
|
|
777
|
+
console.log(` Dashboard context: none yet — stub created, Claude will fill it in`);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
const lines = dashCtxContent.split("\n").length;
|
|
781
|
+
console.log(` Dashboard context: this-dashboard.md (${lines} lines)`);
|
|
782
|
+
}
|
|
783
|
+
if (fs.existsSync(sqlStylePath)) {
|
|
784
|
+
console.log(` SQL style: project override active`);
|
|
785
|
+
}
|
|
786
|
+
console.log(`
|
|
784
787
|
Next steps:
|
|
785
788
|
|
|
786
789
|
cd ${slug}
|
|
@@ -793,7 +796,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
|
793
796
|
`);
|
|
794
797
|
}
|
|
795
798
|
async function versions() {
|
|
796
|
-
const { dashboardSlug } = loadEnv();
|
|
799
|
+
const { dashboardSlug } = await loadEnv();
|
|
797
800
|
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/versions`));
|
|
798
801
|
console.log(`\n Dashboard: ${dashboardSlug}\n`);
|
|
799
802
|
if (data.versions.length === 0) {
|
|
@@ -817,7 +820,7 @@ async function versions() {
|
|
|
817
820
|
console.log(`\n Pull a version: npx @overscore/cli pull ${dashboardSlug} --version N\n`);
|
|
818
821
|
}
|
|
819
822
|
async function listDashboards() {
|
|
820
|
-
const { apiUrl, apiKey } = loadEnv();
|
|
823
|
+
const { apiUrl, apiKey } = await loadEnv();
|
|
821
824
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
822
825
|
const res = await fetch(`${baseUrl}/api/dashboards`, {
|
|
823
826
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
@@ -923,44 +926,80 @@ async function loadAnalysisConfig() {
|
|
|
923
926
|
}
|
|
924
927
|
}
|
|
925
928
|
}
|
|
926
|
-
// (3)
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
929
|
+
// (3) No valid API key — use an existing device token or trigger browser auth
|
|
930
|
+
let existingCfg = fs.existsSync(configPath)
|
|
931
|
+
? parseEnv(fs.readFileSync(configPath, "utf-8"))
|
|
932
|
+
: {};
|
|
933
|
+
let deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
|
|
934
|
+
if (!deviceToken) {
|
|
935
|
+
console.log("\n No Overscore credentials found. Launching browser login...\n");
|
|
936
|
+
await authLogin();
|
|
937
|
+
existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
938
|
+
deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
|
|
939
|
+
if (!deviceToken) {
|
|
940
|
+
console.error("\n Error: Login did not complete. Please try again.\n");
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
932
943
|
}
|
|
933
|
-
//
|
|
934
|
-
let
|
|
935
|
-
|
|
936
|
-
|
|
944
|
+
// Resolve project slug
|
|
945
|
+
let targetProjectSlug = existingCfg.OVERSCORE_PROJECT_SLUG || "";
|
|
946
|
+
if (!targetProjectSlug) {
|
|
947
|
+
targetProjectSlug = await prompt("Project slug:");
|
|
948
|
+
if (!targetProjectSlug) {
|
|
949
|
+
console.error("\n Error: Project slug is required.\n");
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// Generate an API key via device token (retry once on expired token)
|
|
954
|
+
let generatedKey;
|
|
955
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
956
|
+
const keyGenRes = await fetch(`${HUB_URL}/api/projects/${targetProjectSlug}/api-keys`, {
|
|
937
957
|
method: "POST",
|
|
938
|
-
headers: {
|
|
939
|
-
|
|
958
|
+
headers: {
|
|
959
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
960
|
+
"Content-Type": "application/json",
|
|
961
|
+
},
|
|
962
|
+
body: JSON.stringify({ name: `cli-${Date.now()}` }),
|
|
940
963
|
});
|
|
941
|
-
if (
|
|
942
|
-
const data = (await
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
964
|
+
if (keyGenRes.ok) {
|
|
965
|
+
const data = (await keyGenRes.json());
|
|
966
|
+
generatedKey = data.raw_key;
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
if ((keyGenRes.status === 401 || keyGenRes.status === 403) && attempt === 0) {
|
|
970
|
+
console.log("\n Session expired. Launching browser login...\n");
|
|
971
|
+
await authLogin();
|
|
972
|
+
existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
973
|
+
deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
|
|
974
|
+
if (!deviceToken) {
|
|
975
|
+
console.error("\n Error: Login did not complete. Please try again.\n");
|
|
976
|
+
process.exit(1);
|
|
946
977
|
}
|
|
978
|
+
continue;
|
|
947
979
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
projectSlug = await prompt("Project slug:");
|
|
954
|
-
if (!projectSlug) {
|
|
955
|
-
console.error("\n Error: Project slug is required\n");
|
|
956
|
-
process.exit(1);
|
|
980
|
+
if (keyGenRes.status === 403) {
|
|
981
|
+
console.error(`\n Error: Access denied for project "${targetProjectSlug}". Make sure you have access.\n`);
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
console.error(`\n Error: Failed to generate API key (HTTP ${keyGenRes.status}).\n`);
|
|
957
985
|
}
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
if (!generatedKey) {
|
|
989
|
+
console.error("\n Error: Failed to generate API key.\n");
|
|
990
|
+
process.exit(1);
|
|
958
991
|
}
|
|
959
|
-
//
|
|
992
|
+
// Persist the new key
|
|
993
|
+
const newLines = [
|
|
994
|
+
`OVERSCORE_API_URL=${HUB_URL}/api`,
|
|
995
|
+
`OVERSCORE_DEVICE_TOKEN=${deviceToken}`,
|
|
996
|
+
`OVERSCORE_API_KEY=${generatedKey}`,
|
|
997
|
+
`OVERSCORE_PROJECT_SLUG=${targetProjectSlug}`,
|
|
998
|
+
];
|
|
960
999
|
fs.mkdirSync(configDir, { recursive: true });
|
|
961
|
-
fs.writeFileSync(configPath,
|
|
1000
|
+
fs.writeFileSync(configPath, newLines.join("\n") + "\n", { mode: 0o600 });
|
|
962
1001
|
console.log(`\n Saved to ~/.overscore/config\n`);
|
|
963
|
-
return { apiUrl: `${HUB_URL}/api`, apiKey, projectSlug };
|
|
1002
|
+
return { apiUrl: `${HUB_URL}/api`, apiKey: generatedKey, projectSlug: targetProjectSlug };
|
|
964
1003
|
}
|
|
965
1004
|
async function analysisApiRequest(cfg, method, urlPath, body) {
|
|
966
1005
|
const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
|
package/package.json
CHANGED
|
@@ -5,26 +5,32 @@ description: Reviews data insights discovered during dashboard or analysis work
|
|
|
5
5
|
|
|
6
6
|
# Save Knowledge
|
|
7
7
|
|
|
8
|
-
Run
|
|
9
|
-
|
|
10
|
-
## When to invoke
|
|
11
|
-
- Before `npx @overscore/cli deploy`
|
|
12
|
-
- When the user says "save what you learned" or similar
|
|
8
|
+
Run before deploying a dashboard, before publishing an analysis, or whenever you discover something non-obvious about the data.
|
|
13
9
|
|
|
14
10
|
## Process
|
|
15
11
|
|
|
16
|
-
1. Review the
|
|
12
|
+
1. Review the code you've written and queries you've run this session.
|
|
13
|
+
|
|
14
|
+
2. **Read the existing files before proposing anything.** Read `.claude/knowledge/INDEX.md`, then read the specific knowledge files relevant to what you learned. Read `.claude/rules/this-dashboard.md` (or `analysis-log.md` for analyses) in full. You need to know what's already there before you can know what's new.
|
|
15
|
+
|
|
16
|
+
3. For each insight from this session, decide what action to take:
|
|
17
|
+
|
|
18
|
+
| Situation | Action |
|
|
19
|
+
|---|---|
|
|
20
|
+
| New topic, no existing file covers it | Create a new `.claude/knowledge/{topic}.md`; add entry to INDEX.md |
|
|
21
|
+
| New fact that belongs in an existing knowledge file | Append a new line or section to that file — do not rewrite what's there |
|
|
22
|
+
| Fact that refines or corrects something already documented | Edit that specific line/section only — do not duplicate it |
|
|
23
|
+
| Already documented accurately | Skip entirely |
|
|
24
|
+
| Dashboard/analysis-specific insight not yet captured | Append to `this-dashboard.md` or `analysis-log.md` |
|
|
25
|
+
| Insight that updates something already there | Edit that specific part only |
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
**Never overwrite a file wholesale. Never duplicate a fact that's already accurate. Only write what's genuinely new or changed.**
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
- **Project knowledge** (helps ALL future artifacts) → propose addition to `.claude/knowledge/`
|
|
22
|
-
- **Dashboard-specific** (design decision, query note) → append to `.claude/rules/this-dashboard.md`
|
|
23
|
-
- **Already known** → skip
|
|
29
|
+
4. Present proposed changes to the user. Be specific — show the exact text you'd add or the exact edit you'd make. "I should note something about X" is not acceptable — show the line.
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
5. After the user approves, write the files. If you created a new knowledge file, update INDEX.md.
|
|
26
32
|
|
|
27
|
-
## What qualifies as project knowledge vs
|
|
33
|
+
## What qualifies as project knowledge vs artifact-specific
|
|
28
34
|
- "dates are UTC-7 no DST" → project knowledge (helps all artifacts)
|
|
29
35
|
- "affiliate_id = 0 is unattributed" → project knowledge
|
|
30
36
|
- "Revenue chart excludes refunds per Finance definition" → dashboard-specific
|
|
@@ -5,11 +5,7 @@ description: Reviews data insights discovered during analysis work and proposes
|
|
|
5
5
|
|
|
6
6
|
# Save Knowledge
|
|
7
7
|
|
|
8
|
-
Run
|
|
9
|
-
|
|
10
|
-
## When to invoke
|
|
11
|
-
- Before `analysis publish` (review what you learned first)
|
|
12
|
-
- When the user says "save what you learned" or similar
|
|
8
|
+
Run before publishing an analysis or whenever you discover something non-obvious about the data.
|
|
13
9
|
|
|
14
10
|
## Process
|
|
15
11
|
|
|
@@ -19,14 +15,21 @@ Run this skill before publishing an analysis or when explicitly asked.
|
|
|
19
15
|
- Metric definitions or business rules
|
|
20
16
|
- Data quality issues or known gaps
|
|
21
17
|
|
|
22
|
-
2. Read `.claude/knowledge/INDEX.md
|
|
18
|
+
2. **Read the existing knowledge files before proposing anything.** Read `.claude/knowledge/INDEX.md`, then read the specific files relevant to what you discovered. You need to know what's already there before you can know what's new.
|
|
19
|
+
|
|
20
|
+
3. For each insight, decide what action to take:
|
|
21
|
+
|
|
22
|
+
| Situation | Action |
|
|
23
|
+
|---|---|
|
|
24
|
+
| New topic, no existing file covers it | Create a new `.claude/knowledge/{topic}.md`; add entry to INDEX.md |
|
|
25
|
+
| New fact that belongs in an existing knowledge file | Append a new line or section to that file — do not rewrite what's there |
|
|
26
|
+
| Fact that refines or corrects something already documented | Edit that specific line/section only — do not duplicate it |
|
|
27
|
+
| Already documented accurately | Skip entirely |
|
|
28
|
+
| Analysis-specific (only relevant to this investigation) | Leave in analysis-log.md — don't promote to project knowledge |
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
- **Project knowledge** (helps ALL future artifacts) → propose a new file or addition to `.claude/knowledge/`
|
|
26
|
-
- **Already known** (in an existing knowledge file) → skip
|
|
27
|
-
- **Analysis-specific** (only relevant to this investigation) → leave in analysis-log.md
|
|
30
|
+
**Never overwrite a file wholesale. Never duplicate a fact that's already accurate. Only write what's genuinely new or changed.**
|
|
28
31
|
|
|
29
|
-
4. Present proposed
|
|
32
|
+
4. Present proposed changes to the user. Show the exact text you'd add or the exact edit you'd make — not a summary of what you intend to write.
|
|
30
33
|
|
|
31
34
|
5. After approval, write the files and update INDEX.md.
|
|
32
35
|
|
|
@@ -34,5 +37,5 @@ Run this skill before publishing an analysis or when explicitly asked.
|
|
|
34
37
|
- "unique_page_views is already COUNT(DISTINCT domain_userid)" → YES
|
|
35
38
|
- "date_az is America/Phoenix UTC-7, no DST" → YES
|
|
36
39
|
- "affiliate_id = 0 is unattributed, usually exclude" → YES
|
|
37
|
-
- "Q1 churn spiked because of the pricing change" → NO (analysis conclusion, not data fact)
|
|
40
|
+
- "Q1 churn spiked because of the pricing change" → NO (analysis conclusion, not a data fact)
|
|
38
41
|
- "I used a 90-day window for this analysis" → NO (analysis-specific decision)
|