@overscore/cli 0.10.0 → 0.10.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 +229 -57
- package/package.json +1 -1
- package/skills/save-knowledge/SKILL.md +31 -0
- package/skills/scaffold-overscore-artifact/SKILL.md +3 -1
- package/templates/analysis/.claude/CLAUDE.md +14 -3
- package/templates/analysis/.claude/rules/report-html.md +7 -0
- package/templates/analysis/.claude/skills/save-knowledge/SKILL.md +38 -0
- package/templates/analysis/.claude/skills/start-analysis/SKILL.md +8 -3
- package/templates/analysis/_overscoreignore +1 -0
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ function loadEnv() {
|
|
|
72
72
|
}
|
|
73
73
|
process.exit(1);
|
|
74
74
|
}
|
|
75
|
-
async function apiRequest(method, urlPath, body) {
|
|
75
|
+
async function apiRequest(method, urlPath, body, command) {
|
|
76
76
|
const { apiUrl, apiKey } = loadEnv();
|
|
77
77
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
78
78
|
const url = `${baseUrl}${urlPath}`;
|
|
@@ -82,6 +82,9 @@ async function apiRequest(method, urlPath, body) {
|
|
|
82
82
|
if (body !== undefined) {
|
|
83
83
|
headers["Content-Type"] = "application/json";
|
|
84
84
|
}
|
|
85
|
+
if (command) {
|
|
86
|
+
headers["X-Overscore-Command"] = command;
|
|
87
|
+
}
|
|
85
88
|
const res = await fetch(url, {
|
|
86
89
|
method,
|
|
87
90
|
headers,
|
|
@@ -261,7 +264,10 @@ async function deploy() {
|
|
|
261
264
|
// Sync context files to Hub
|
|
262
265
|
const { projectSlug } = loadEnv();
|
|
263
266
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
264
|
-
//
|
|
267
|
+
// Upload knowledge base (project-level, shared across all artifacts)
|
|
268
|
+
await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
|
|
269
|
+
// Sync company context (with conflict detection) — legacy path for
|
|
270
|
+
// projects that haven't migrated to the knowledge base yet.
|
|
265
271
|
const companyCtxPath = path.resolve(process.cwd(), ".claude/rules/company-context.md");
|
|
266
272
|
if (fs.existsSync(companyCtxPath)) {
|
|
267
273
|
const content = fs.readFileSync(companyCtxPath, "utf-8");
|
|
@@ -304,9 +310,11 @@ async function deploy() {
|
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
// Sync dashboard context (no conflict detection needed — per-dashboard)
|
|
307
|
-
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard
|
|
308
|
-
|
|
309
|
-
|
|
313
|
+
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
|
|
314
|
+
const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
|
|
315
|
+
const activeDashCtxPath = fs.existsSync(dashCtxPath) ? dashCtxPath : fs.existsSync(legacyDashCtxPath) ? legacyDashCtxPath : null;
|
|
316
|
+
if (activeDashCtxPath) {
|
|
317
|
+
const content = fs.readFileSync(activeDashCtxPath, "utf-8");
|
|
310
318
|
try {
|
|
311
319
|
await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
|
|
312
320
|
method: "PUT",
|
|
@@ -327,23 +335,45 @@ async function deploy() {
|
|
|
327
335
|
}
|
|
328
336
|
async function queryList() {
|
|
329
337
|
const { dashboardSlug } = loadEnv();
|
|
330
|
-
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries
|
|
338
|
+
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
|
|
331
339
|
console.log(`\n Dashboard: ${data.dashboard}\n`);
|
|
340
|
+
// Registered queries
|
|
332
341
|
if (data.queries.length === 0) {
|
|
333
342
|
console.log(" No queries registered yet.\n");
|
|
334
|
-
console.log(" Add
|
|
335
|
-
console.log(' npx @overscore/cli query add <name> "SELECT ..."\n');
|
|
336
|
-
return;
|
|
343
|
+
console.log(" Add a SQL query: npx @overscore/cli query add <name> \"SELECT ...\"\n");
|
|
337
344
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.log(`
|
|
345
|
+
else {
|
|
346
|
+
console.log(" Registered Queries:\n");
|
|
347
|
+
for (const q of data.queries) {
|
|
348
|
+
const rowInfo = q.row_count !== null ? ` (~${q.row_count.toLocaleString()} rows)` : "";
|
|
349
|
+
const sourceTag = q.source === "dataset" ? " [dataset]" : "";
|
|
350
|
+
console.log(` ${q.query_name}${rowInfo}${sourceTag}`);
|
|
351
|
+
console.log(` useQuery("${q.query_name}")`);
|
|
352
|
+
if (q.sample_row) {
|
|
353
|
+
const cols = Object.keys(q.sample_row).join(", ");
|
|
354
|
+
console.log(` columns: ${cols}`);
|
|
355
|
+
}
|
|
356
|
+
else if (q.columns && q.columns.length > 0) {
|
|
357
|
+
const cols = q.columns.map((c) => `${c.name} (${c.type})`).join(", ");
|
|
358
|
+
console.log(` columns: ${cols}`);
|
|
359
|
+
}
|
|
360
|
+
console.log();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Project datasets (always shown)
|
|
364
|
+
if (data.project_datasets && data.project_datasets.length > 0) {
|
|
365
|
+
console.log(" Project Datasets:\n");
|
|
366
|
+
for (const ds of data.project_datasets) {
|
|
367
|
+
const rowInfo = ds.row_count !== null ? ` (~${ds.row_count.toLocaleString()} rows)` : "";
|
|
368
|
+
const sampleTag = ds.is_sample ? " [sample]" : "";
|
|
369
|
+
console.log(` ${ds.name}${rowInfo}${sampleTag}`);
|
|
370
|
+
if (ds.columns && ds.columns.length > 0) {
|
|
371
|
+
const cols = ds.columns.map((c) => `${c.name} (${c.type})`).join(", ");
|
|
372
|
+
console.log(` columns: ${cols}`);
|
|
373
|
+
}
|
|
374
|
+
console.log(` To use: npx @overscore/cli query add <query-name> --dataset ${ds.name}`);
|
|
375
|
+
console.log();
|
|
345
376
|
}
|
|
346
|
-
console.log();
|
|
347
377
|
}
|
|
348
378
|
}
|
|
349
379
|
async function queryShow(name) {
|
|
@@ -365,16 +395,28 @@ async function queryShow(name) {
|
|
|
365
395
|
}
|
|
366
396
|
console.log(`\n ${query.query_name}`);
|
|
367
397
|
if (query.row_count !== null) {
|
|
368
|
-
console.log(` ~${query.row_count.toLocaleString()} rows
|
|
398
|
+
console.log(` ~${query.row_count.toLocaleString()} rows`);
|
|
369
399
|
}
|
|
370
|
-
|
|
371
|
-
console.log();
|
|
400
|
+
if (query.source === "dataset" && query.dataset_name) {
|
|
401
|
+
console.log(` source: uploaded dataset (${query.dataset_name})`);
|
|
372
402
|
}
|
|
373
|
-
console.log(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
403
|
+
console.log();
|
|
404
|
+
if (query.source === "dataset") {
|
|
405
|
+
if (query.columns && query.columns.length > 0) {
|
|
406
|
+
console.log(` Columns:`);
|
|
407
|
+
for (const col of query.columns) {
|
|
408
|
+
console.log(` ${col.name} (${col.type})`);
|
|
409
|
+
}
|
|
410
|
+
console.log();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (query.sql) {
|
|
414
|
+
console.log(` SQL:`);
|
|
415
|
+
console.log(` ${query.sql}\n`);
|
|
416
|
+
if (query.sample_row) {
|
|
417
|
+
console.log(` Sample row:`);
|
|
418
|
+
console.log(` ${JSON.stringify(query.sample_row, null, 2).split("\n").join("\n ")}\n`);
|
|
419
|
+
}
|
|
378
420
|
}
|
|
379
421
|
}
|
|
380
422
|
function resolveSql(sqlOrNext) {
|
|
@@ -391,10 +433,15 @@ function resolveSql(sqlOrNext) {
|
|
|
391
433
|
return sqlOrNext;
|
|
392
434
|
}
|
|
393
435
|
async function queryAdd(name, sql) {
|
|
436
|
+
// Check for --dataset flag
|
|
437
|
+
const datasetFlagIndex = process.argv.indexOf("--dataset");
|
|
438
|
+
const datasetName = datasetFlagIndex !== -1 ? process.argv[datasetFlagIndex + 1] : undefined;
|
|
394
439
|
const resolvedSql = resolveSql(sql);
|
|
395
|
-
if (!name || !resolvedSql) {
|
|
440
|
+
if (!name || (!resolvedSql && !datasetName)) {
|
|
396
441
|
console.error('\n Usage: npx @overscore/cli query add <name> "<sql>"\n');
|
|
397
|
-
console.error(" Or
|
|
442
|
+
console.error(" Or link an uploaded dataset:");
|
|
443
|
+
console.error(" npx @overscore/cli query add <name> --dataset <dataset-name>\n");
|
|
444
|
+
console.error(" Or with a SQL file:");
|
|
398
445
|
console.error(" npx @overscore/cli query add <name> --file query.sql\n");
|
|
399
446
|
process.exit(1);
|
|
400
447
|
}
|
|
@@ -403,7 +450,26 @@ async function queryAdd(name, sql) {
|
|
|
403
450
|
process.exit(1);
|
|
404
451
|
}
|
|
405
452
|
const { dashboardSlug } = loadEnv();
|
|
406
|
-
|
|
453
|
+
let body;
|
|
454
|
+
if (datasetName) {
|
|
455
|
+
// Look up the dataset UUID from project_datasets in the queries endpoint
|
|
456
|
+
const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-add"));
|
|
457
|
+
const dataset = (listData.project_datasets ?? []).find((ds) => ds.name === datasetName);
|
|
458
|
+
if (!dataset) {
|
|
459
|
+
console.error(`\n Error: Dataset "${datasetName}" not found in this project.\n`);
|
|
460
|
+
console.error(" Available datasets:");
|
|
461
|
+
for (const ds of listData.project_datasets ?? []) {
|
|
462
|
+
console.error(` - ${ds.name}`);
|
|
463
|
+
}
|
|
464
|
+
console.error();
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
body = { query_name: name, dataset_id: dataset.id };
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
body = { query_name: name, sql_text: resolvedSql };
|
|
471
|
+
}
|
|
472
|
+
const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body, "query-add"));
|
|
407
473
|
console.log(`\n Query "${data.query_name}" created.\n`);
|
|
408
474
|
console.log(` Use it in your dashboard:`);
|
|
409
475
|
console.log(` const { data } = useQuery("${data.query_name}");\n`);
|
|
@@ -435,24 +501,34 @@ async function queryRemove(name) {
|
|
|
435
501
|
await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
|
|
436
502
|
console.log(`\n Query "${name}" deleted.\n`);
|
|
437
503
|
}
|
|
438
|
-
function saveQueryResult(sql, data) {
|
|
504
|
+
function saveQueryResult(sql, data, sourceFile) {
|
|
439
505
|
try {
|
|
440
506
|
const hash = createHash("sha256").update(sql).digest("hex").slice(0, 8);
|
|
441
|
-
const snippet = sql
|
|
442
|
-
.toLowerCase()
|
|
443
|
-
.replace(/[^a-z0-9_]+/g, "_")
|
|
444
|
-
.replace(/^_|_$/g, "")
|
|
445
|
-
.slice(0, 40);
|
|
446
|
-
const filename = `${hash}_${snippet}.json`;
|
|
447
507
|
const dataDir = path.resolve(process.cwd(), "data");
|
|
508
|
+
// Check for ANY existing file with this hash — handles both old and new
|
|
509
|
+
// filename formats so the same query isn't duplicated.
|
|
510
|
+
if (fs.existsSync(dataDir)) {
|
|
511
|
+
const existing = fs.readdirSync(dataDir).find((f) => f.includes(hash) && f.endsWith(".json"));
|
|
512
|
+
if (existing) {
|
|
513
|
+
return { path: `data/${existing}`, cached: true };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Build filename: human-readable prefix when from a file, hash prefix otherwise
|
|
517
|
+
let filename;
|
|
518
|
+
if (sourceFile) {
|
|
519
|
+
const baseName = path.basename(sourceFile, path.extname(sourceFile));
|
|
520
|
+
filename = `${baseName}__${hash}.json`;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
const snippet = sql
|
|
524
|
+
.toLowerCase()
|
|
525
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
526
|
+
.replace(/^_|_$/g, "")
|
|
527
|
+
.slice(0, 40);
|
|
528
|
+
filename = `${hash}_${snippet}.json`;
|
|
529
|
+
}
|
|
448
530
|
const fullPath = path.join(dataDir, filename);
|
|
449
531
|
const relPath = `data/${filename}`;
|
|
450
|
-
// Hash is content-derived from the SQL — if a file with this hash already
|
|
451
|
-
// exists, the exact same query was run before. Skip the rewrite so the
|
|
452
|
-
// data/ folder doesn't accumulate duplicates of identical queries.
|
|
453
|
-
if (fs.existsSync(fullPath)) {
|
|
454
|
-
return { path: relPath, cached: true };
|
|
455
|
-
}
|
|
456
532
|
if (!fs.existsSync(dataDir)) {
|
|
457
533
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
458
534
|
}
|
|
@@ -501,7 +577,9 @@ async function queryTest(sql) {
|
|
|
501
577
|
process.exit(1);
|
|
502
578
|
}
|
|
503
579
|
const data = (await apiRequest("POST", `/api/projects/${projectSlug}/test-query`, { sql: resolvedSql }));
|
|
504
|
-
const
|
|
580
|
+
const fileIndex = process.argv.indexOf("--file");
|
|
581
|
+
const sourceFile = fileIndex !== -1 ? process.argv[fileIndex + 1] : undefined;
|
|
582
|
+
const saved = saveQueryResult(resolvedSql, data, sourceFile);
|
|
505
583
|
const savedLine = saved
|
|
506
584
|
? saved.cached
|
|
507
585
|
? ` Cached (already saved earlier): ${saved.path}`
|
|
@@ -657,7 +735,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
|
657
735
|
if (dashCtxData.context_md) {
|
|
658
736
|
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
659
737
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
660
|
-
fs.writeFileSync(path.join(rulesDir, "dashboard
|
|
738
|
+
fs.writeFileSync(path.join(rulesDir, "this-dashboard.md"), dashCtxData.context_md);
|
|
661
739
|
console.log(" Updated dashboard context from Hub.");
|
|
662
740
|
}
|
|
663
741
|
}
|
|
@@ -889,19 +967,23 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
889
967
|
const authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
890
968
|
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
891
969
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
970
|
+
// Try knowledge base first — if the project has knowledge files, they
|
|
971
|
+
// replace the legacy company-context.md.
|
|
972
|
+
const hasKnowledge = await syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey);
|
|
973
|
+
// Company context — only fetch if no knowledge files exist (legacy path).
|
|
974
|
+
if (!hasKnowledge) {
|
|
975
|
+
try {
|
|
976
|
+
const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
|
|
977
|
+
if (ctxRes.ok) {
|
|
978
|
+
const ctxData = (await ctxRes.json());
|
|
979
|
+
if (ctxData.context_md) {
|
|
980
|
+
fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
|
|
981
|
+
}
|
|
900
982
|
}
|
|
901
983
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
984
|
+
catch {
|
|
985
|
+
// Non-fatal
|
|
986
|
+
}
|
|
905
987
|
}
|
|
906
988
|
// SQL style override — null means "use the CLI default"; leave the
|
|
907
989
|
// template/bundle file alone in that case.
|
|
@@ -918,6 +1000,93 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
918
1000
|
// Non-fatal
|
|
919
1001
|
}
|
|
920
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Sync the project knowledge base from the Hub into .claude/knowledge/.
|
|
1005
|
+
* Returns true if knowledge files were found (caller can skip legacy
|
|
1006
|
+
* company-context.md in that case).
|
|
1007
|
+
*/
|
|
1008
|
+
async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
1009
|
+
const authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
1010
|
+
const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
|
|
1011
|
+
try {
|
|
1012
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, { headers: authHeader });
|
|
1013
|
+
if (!res.ok)
|
|
1014
|
+
return false;
|
|
1015
|
+
const data = (await res.json());
|
|
1016
|
+
if (!data.files || data.files.length === 0)
|
|
1017
|
+
return false;
|
|
1018
|
+
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1019
|
+
// Write each knowledge file
|
|
1020
|
+
for (const file of data.files) {
|
|
1021
|
+
fs.writeFileSync(path.join(knowledgeDir, file.path), file.content);
|
|
1022
|
+
}
|
|
1023
|
+
// Generate INDEX.md
|
|
1024
|
+
const indexLines = [
|
|
1025
|
+
"# Project Knowledge Base",
|
|
1026
|
+
"",
|
|
1027
|
+
"Read the relevant file when working on a topic. Do NOT read all files every session.",
|
|
1028
|
+
"",
|
|
1029
|
+
];
|
|
1030
|
+
for (const file of data.files) {
|
|
1031
|
+
indexLines.push(`- **${file.title}**: \`.claude/knowledge/${file.path}\``);
|
|
1032
|
+
}
|
|
1033
|
+
indexLines.push("");
|
|
1034
|
+
fs.writeFileSync(path.join(knowledgeDir, "INDEX.md"), indexLines.join("\n"));
|
|
1035
|
+
// Store hashes for conflict detection on upload
|
|
1036
|
+
const hashMap = {};
|
|
1037
|
+
for (const file of data.files) {
|
|
1038
|
+
hashMap[file.path] = file.content_hash;
|
|
1039
|
+
}
|
|
1040
|
+
fs.writeFileSync(path.join(knowledgeDir, ".knowledge-hashes"), JSON.stringify(hashMap, null, 2));
|
|
1041
|
+
console.log(` Knowledge base synced (${data.files.length} files).`);
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Upload local knowledge files back to the Hub.
|
|
1050
|
+
* Called from deploy/publish so knowledge learned during a session persists.
|
|
1051
|
+
*/
|
|
1052
|
+
async function uploadKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
1053
|
+
const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
|
|
1054
|
+
if (!fs.existsSync(knowledgeDir))
|
|
1055
|
+
return;
|
|
1056
|
+
const files = [];
|
|
1057
|
+
for (const entry of fs.readdirSync(knowledgeDir)) {
|
|
1058
|
+
// Skip INDEX.md (auto-generated) and dotfiles (.knowledge-hashes)
|
|
1059
|
+
if (entry === "INDEX.md" ||
|
|
1060
|
+
entry.startsWith(".") ||
|
|
1061
|
+
!entry.endsWith(".md"))
|
|
1062
|
+
continue;
|
|
1063
|
+
const content = fs.readFileSync(path.join(knowledgeDir, entry), "utf-8");
|
|
1064
|
+
// Extract title from first H1 line, fallback to filename
|
|
1065
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1066
|
+
const title = titleMatch
|
|
1067
|
+
? titleMatch[1]
|
|
1068
|
+
: entry.replace(/\.md$/, "").replace(/[-_]/g, " ");
|
|
1069
|
+
files.push({ path: entry, title, content });
|
|
1070
|
+
}
|
|
1071
|
+
if (files.length === 0)
|
|
1072
|
+
return;
|
|
1073
|
+
try {
|
|
1074
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, {
|
|
1075
|
+
method: "PUT",
|
|
1076
|
+
headers: {
|
|
1077
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1078
|
+
"Content-Type": "application/json",
|
|
1079
|
+
},
|
|
1080
|
+
body: JSON.stringify({ files }),
|
|
1081
|
+
});
|
|
1082
|
+
if (res.ok) {
|
|
1083
|
+
console.log(` Knowledge base synced (${files.length} files).`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
// Non-fatal
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
921
1090
|
/**
|
|
922
1091
|
* Fetch the latest platform-owned Claude Code rules from the Hub and write
|
|
923
1092
|
* them into the project's .claude/ directory. This keeps rules like
|
|
@@ -925,8 +1094,8 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
925
1094
|
* projects that were scaffolded months ago.
|
|
926
1095
|
*
|
|
927
1096
|
* Called from `deploy` (before uploading) and `pull` (after extracting).
|
|
928
|
-
* User-owned files (company-context.md, dashboard
|
|
929
|
-
* touched — those sync via separate mechanisms.
|
|
1097
|
+
* User-owned files (company-context.md, this-dashboard.md, knowledge/)
|
|
1098
|
+
* are NOT touched — those sync via separate mechanisms.
|
|
930
1099
|
*/
|
|
931
1100
|
async function syncPlatformRules(targetDir, baseUrl, apiKey) {
|
|
932
1101
|
try {
|
|
@@ -1273,6 +1442,8 @@ async function analysisPublish() {
|
|
|
1273
1442
|
console.log(`\n Published successfully!\n`);
|
|
1274
1443
|
printBundleSummary(files, process.cwd());
|
|
1275
1444
|
console.log(`\n URL: ${data.url}\n`);
|
|
1445
|
+
// Upload knowledge base so insights from this analysis persist
|
|
1446
|
+
await uploadKnowledgeBase(process.cwd(), baseUrl, meta.project_slug, cfg.apiKey);
|
|
1276
1447
|
}
|
|
1277
1448
|
/**
|
|
1278
1449
|
* Ensure analysis.json has a valid id by fetching from the Hub.
|
|
@@ -1844,7 +2015,8 @@ else if (command === "query") {
|
|
|
1844
2015
|
Commands:
|
|
1845
2016
|
list List all queries
|
|
1846
2017
|
show <name> Show a query's SQL and sample data
|
|
1847
|
-
add <name> "<sql>" Add a
|
|
2018
|
+
add <name> "<sql>" Add a SQL query
|
|
2019
|
+
add <name> --dataset <name> Link an uploaded dataset as a query
|
|
1848
2020
|
update <name> "<sql>" Update a query's SQL
|
|
1849
2021
|
remove <name> Delete a query
|
|
1850
2022
|
run "<sql>" Run SQL against BigQuery
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: save-knowledge
|
|
3
|
+
description: Reviews data insights discovered during dashboard or analysis work and proposes additions to the project knowledge base. Use before deploying a dashboard or publishing an analysis, when the user says "save what you learned", or after discovering non-obvious data facts like timezone quirks, column semantics, dedup rules, or metric definitions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Save Knowledge
|
|
7
|
+
|
|
8
|
+
Run this skill before deploying a dashboard or when explicitly asked.
|
|
9
|
+
|
|
10
|
+
## When to invoke
|
|
11
|
+
- Before `npx @overscore/cli deploy`
|
|
12
|
+
- When the user says "save what you learned" or similar
|
|
13
|
+
|
|
14
|
+
## Process
|
|
15
|
+
|
|
16
|
+
1. Review the dashboard code and any exploratory queries you ran.
|
|
17
|
+
|
|
18
|
+
2. Read `.claude/knowledge/INDEX.md` to see what's already documented.
|
|
19
|
+
|
|
20
|
+
3. For each new insight, decide:
|
|
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
|
|
24
|
+
|
|
25
|
+
4. Present proposed additions to the user for approval before writing any files.
|
|
26
|
+
|
|
27
|
+
## What qualifies as project knowledge vs dashboard-specific
|
|
28
|
+
- "dates are UTC-7 no DST" → project knowledge (helps all artifacts)
|
|
29
|
+
- "affiliate_id = 0 is unattributed" → project knowledge
|
|
30
|
+
- "Revenue chart excludes refunds per Finance definition" → dashboard-specific
|
|
31
|
+
- "Default filter is 90 days for CFO's current-quarter view" → dashboard-specific
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scaffold-overscore-artifact
|
|
3
|
-
description:
|
|
3
|
+
description: Routes users to the correct Overscore artifact type (Dashboard vs Analysis) and scaffolds it in the right location. Dashboards track metrics forever with live data; Analyses answer a question once with a static HTML report.
|
|
4
|
+
when_to_use: Use when the user wants to create a new dashboard or analysis, or when they're inside one artifact type and want to switch to the other. Trigger phrases include "build a dashboard", "track X over time", "create an analysis", "investigate why X", "turn this into a dashboard", "do a quick analysis", "I want to monitor", "I need a report on".
|
|
5
|
+
argument-hint: "[dashboard or analysis description]"
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
# Scaffolding Overscore Artifacts
|
|
@@ -51,9 +51,20 @@ The CLI authenticates via `~/.overscore/config` (set up on first use). Credentia
|
|
|
51
51
|
|
|
52
52
|
The `queries/` directory is for SQL files you want to keep around — the canonical version of each Q-numbered query in `analysis-log.md`, queries the user might want to re-run later, or anything you've iterated on enough to deserve a name. File names like `q3-cohort-halo.sql` cross-reference the Query Log. Exploratory one-offs don't need to live here; they're already saved as JSON in `data/`.
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## Project Knowledge Base
|
|
55
55
|
|
|
56
|
-
`.claude/
|
|
56
|
+
`.claude/knowledge/INDEX.md` lists all knowledge files for this project — table definitions,
|
|
57
|
+
business logic, metric formulas, data quirks. **Read INDEX.md before starting any data work.**
|
|
58
|
+
Only read the specific files relevant to your current task.
|
|
59
|
+
|
|
60
|
+
When you discover something non-obvious (a timezone gotcha, a dedup rule, a column that means
|
|
61
|
+
something unexpected), **propose adding it to the knowledge base**:
|
|
62
|
+
- Create a new `.claude/knowledge/{topic}.md` file (one topic per file, 20-80 lines)
|
|
63
|
+
- Or append to an existing file if the topic matches
|
|
64
|
+
- Update INDEX.md when you add or remove a file
|
|
65
|
+
|
|
66
|
+
Knowledge files sync to the Hub on `analysis publish` and are shared with every artifact
|
|
67
|
+
in this project. Good knowledge notes help every future session.
|
|
57
68
|
|
|
58
69
|
## Previewing
|
|
59
70
|
|
|
@@ -96,4 +107,4 @@ The following files in `.claude/rules/` load automatically when you touch matchi
|
|
|
96
107
|
- **`report-html.md`** — Self-contained HTML rules and dark-theme spec (loads when editing `report.html`)
|
|
97
108
|
- **`report-components.md`** — Copy-paste HTML/SVG component snippets: KPI cards, charts, tables, heatmaps (loads when editing `report.html`)
|
|
98
109
|
- **`analysis-methodology.md`** — Investigation loop, when to ask vs query (loads when editing `analysis-log.md`)
|
|
99
|
-
- **`
|
|
110
|
+
- **`knowledge/INDEX.md`** — Project knowledge base catalog (read before data work)
|
|
@@ -80,4 +80,11 @@ A good analysis report has these sections in order:
|
|
|
80
80
|
</html>
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
**Mobile responsiveness.** Reports are often opened on phones from Slack links. The skeleton's `max-width: 900px` and viewport meta tag handle the basics, but also:
|
|
84
|
+
- Use `clamp()` or media queries for KPI font sizes (`font-size: clamp(1.5rem, 5vw, 2.5rem)`) so they don't overflow on 375px screens.
|
|
85
|
+
- KPI card grids should wrap: `display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr))`.
|
|
86
|
+
- Tables need `overflow-x: auto` on their container.
|
|
87
|
+
- Padding: `2rem` is fine on desktop but use `padding: 2rem 1rem` on mobile via `@media (max-width: 480px)`.
|
|
88
|
+
- Charts (SVG): set `width="100%" viewBox="..."` and let them scale, or use `max-width: 100%`.
|
|
89
|
+
|
|
83
90
|
**Always preview `report.html` in a browser and get the user's approval before publishing.**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: save-knowledge
|
|
3
|
+
description: Reviews data insights discovered during analysis work and proposes additions to the project knowledge base. Use before publishing an analysis, when the user says "save what you learned", or after discovering non-obvious data facts like timezone quirks, column semantics, dedup rules, or metric definitions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Save Knowledge
|
|
7
|
+
|
|
8
|
+
Run this skill before publishing an analysis or when explicitly asked.
|
|
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
|
|
13
|
+
|
|
14
|
+
## Process
|
|
15
|
+
|
|
16
|
+
1. Review `analysis-log.md` — look for non-obvious data facts you discovered:
|
|
17
|
+
- Column semantics that weren't obvious from the name
|
|
18
|
+
- Timezone, dedup, or join gotchas
|
|
19
|
+
- Metric definitions or business rules
|
|
20
|
+
- Data quality issues or known gaps
|
|
21
|
+
|
|
22
|
+
2. Read `.claude/knowledge/INDEX.md` to see what's already documented.
|
|
23
|
+
|
|
24
|
+
3. For each new insight, decide:
|
|
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
|
|
28
|
+
|
|
29
|
+
4. Present proposed additions to the user for approval before writing any files.
|
|
30
|
+
|
|
31
|
+
5. After approval, write the files and update INDEX.md.
|
|
32
|
+
|
|
33
|
+
## What qualifies as project knowledge
|
|
34
|
+
- "unique_page_views is already COUNT(DISTINCT domain_userid)" → YES
|
|
35
|
+
- "date_az is America/Phoenix UTC-7, no DST" → YES
|
|
36
|
+
- "affiliate_id = 0 is unattributed, usually exclude" → YES
|
|
37
|
+
- "Q1 churn spiked because of the pricing change" → NO (analysis conclusion, not data fact)
|
|
38
|
+
- "I used a 90-day window for this analysis" → NO (analysis-specific decision)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: start-analysis
|
|
3
|
-
description:
|
|
3
|
+
description: Interviews the user before querying data at the start of a new Overscore analysis. Captures the real question, known tables, success criteria, hypotheses, time window, and data quirks, then writes an initial analysis-log.md entry and proposes a query plan for approval. Use when analysis-log.md has no queries logged yet, or when the user says "investigate", "look into", or "analyze" something for the first time.
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Bash(npx @overscore/cli query run *)
|
|
4
9
|
---
|
|
5
10
|
|
|
6
11
|
# Starting an analysis the right way
|
|
@@ -32,7 +37,7 @@ Ask these one or two at a time conversationally — don't dump all 6 at once lik
|
|
|
32
37
|
|
|
33
38
|
## After the interview
|
|
34
39
|
|
|
35
|
-
1. **Read `.claude/
|
|
40
|
+
1. **Read `.claude/knowledge/INDEX.md`** and any relevant knowledge files to understand what previous sessions have already documented about this project's data. Cross-reference what the user said against what's already known.
|
|
36
41
|
2. **Draft an initial entry in `analysis-log.md`** with:
|
|
37
42
|
- The clarified question (not the original title)
|
|
38
43
|
- The tables the user named, with what each contains
|
|
@@ -51,4 +56,4 @@ Ask these one or two at a time conversationally — don't dump all 6 at once lik
|
|
|
51
56
|
|
|
52
57
|
## If the user resists the interview
|
|
53
58
|
|
|
54
|
-
Some users will say "just look at the data, I'll tell you if it's wrong." That's a trap — you'll spend 30 minutes probing tables and still need the interview at the end. Push back once: "I can save us a lot of time if you tell me what tables matter and what success looks like — it's a 2-minute conversation." If they still refuse, do a single sizing pass on the parent project's known tables (from
|
|
59
|
+
Some users will say "just look at the data, I'll tell you if it's wrong." That's a trap — you'll spend 30 minutes probing tables and still need the interview at the end. Push back once: "I can save us a lot of time if you tell me what tables matter and what success looks like — it's a 2-minute conversation." If they still refuse, do a single sizing pass on the parent project's known tables (from the knowledge base), then come back and ask narrower questions based on what you found.
|