@overscore/cli 0.10.0 → 0.10.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/dist/index.js +224 -55
- 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
|
@@ -261,7 +261,10 @@ async function deploy() {
|
|
|
261
261
|
// Sync context files to Hub
|
|
262
262
|
const { projectSlug } = loadEnv();
|
|
263
263
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
264
|
-
//
|
|
264
|
+
// Upload knowledge base (project-level, shared across all artifacts)
|
|
265
|
+
await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
|
|
266
|
+
// Sync company context (with conflict detection) — legacy path for
|
|
267
|
+
// projects that haven't migrated to the knowledge base yet.
|
|
265
268
|
const companyCtxPath = path.resolve(process.cwd(), ".claude/rules/company-context.md");
|
|
266
269
|
if (fs.existsSync(companyCtxPath)) {
|
|
267
270
|
const content = fs.readFileSync(companyCtxPath, "utf-8");
|
|
@@ -304,9 +307,11 @@ async function deploy() {
|
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
// Sync dashboard context (no conflict detection needed — per-dashboard)
|
|
307
|
-
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
|
|
311
|
+
const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
|
|
312
|
+
const activeDashCtxPath = fs.existsSync(dashCtxPath) ? dashCtxPath : fs.existsSync(legacyDashCtxPath) ? legacyDashCtxPath : null;
|
|
313
|
+
if (activeDashCtxPath) {
|
|
314
|
+
const content = fs.readFileSync(activeDashCtxPath, "utf-8");
|
|
310
315
|
try {
|
|
311
316
|
await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
|
|
312
317
|
method: "PUT",
|
|
@@ -329,21 +334,43 @@ async function queryList() {
|
|
|
329
334
|
const { dashboardSlug } = loadEnv();
|
|
330
335
|
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
|
|
331
336
|
console.log(`\n Dashboard: ${data.dashboard}\n`);
|
|
337
|
+
// Registered queries
|
|
332
338
|
if (data.queries.length === 0) {
|
|
333
339
|
console.log(" No queries registered yet.\n");
|
|
334
|
-
console.log(" Add
|
|
335
|
-
console.log(' npx @overscore/cli query add <name> "SELECT ..."\n');
|
|
336
|
-
return;
|
|
340
|
+
console.log(" Add a SQL query: npx @overscore/cli query add <name> \"SELECT ...\"\n");
|
|
337
341
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.log(`
|
|
342
|
+
else {
|
|
343
|
+
console.log(" Registered Queries:\n");
|
|
344
|
+
for (const q of data.queries) {
|
|
345
|
+
const rowInfo = q.row_count !== null ? ` (~${q.row_count.toLocaleString()} rows)` : "";
|
|
346
|
+
const sourceTag = q.source === "dataset" ? " [dataset]" : "";
|
|
347
|
+
console.log(` ${q.query_name}${rowInfo}${sourceTag}`);
|
|
348
|
+
console.log(` useQuery("${q.query_name}")`);
|
|
349
|
+
if (q.sample_row) {
|
|
350
|
+
const cols = Object.keys(q.sample_row).join(", ");
|
|
351
|
+
console.log(` columns: ${cols}`);
|
|
352
|
+
}
|
|
353
|
+
else if (q.columns && q.columns.length > 0) {
|
|
354
|
+
const cols = q.columns.map((c) => `${c.name} (${c.type})`).join(", ");
|
|
355
|
+
console.log(` columns: ${cols}`);
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Project datasets (always shown)
|
|
361
|
+
if (data.project_datasets && data.project_datasets.length > 0) {
|
|
362
|
+
console.log(" Project Datasets:\n");
|
|
363
|
+
for (const ds of data.project_datasets) {
|
|
364
|
+
const rowInfo = ds.row_count !== null ? ` (~${ds.row_count.toLocaleString()} rows)` : "";
|
|
365
|
+
const sampleTag = ds.is_sample ? " [sample]" : "";
|
|
366
|
+
console.log(` ${ds.name}${rowInfo}${sampleTag}`);
|
|
367
|
+
if (ds.columns && ds.columns.length > 0) {
|
|
368
|
+
const cols = ds.columns.map((c) => `${c.name} (${c.type})`).join(", ");
|
|
369
|
+
console.log(` columns: ${cols}`);
|
|
370
|
+
}
|
|
371
|
+
console.log(` To use: npx @overscore/cli query add <query-name> --dataset ${ds.name}`);
|
|
372
|
+
console.log();
|
|
345
373
|
}
|
|
346
|
-
console.log();
|
|
347
374
|
}
|
|
348
375
|
}
|
|
349
376
|
async function queryShow(name) {
|
|
@@ -365,16 +392,28 @@ async function queryShow(name) {
|
|
|
365
392
|
}
|
|
366
393
|
console.log(`\n ${query.query_name}`);
|
|
367
394
|
if (query.row_count !== null) {
|
|
368
|
-
console.log(` ~${query.row_count.toLocaleString()} rows
|
|
395
|
+
console.log(` ~${query.row_count.toLocaleString()} rows`);
|
|
369
396
|
}
|
|
370
|
-
|
|
371
|
-
console.log();
|
|
397
|
+
if (query.source === "dataset" && query.dataset_name) {
|
|
398
|
+
console.log(` source: uploaded dataset (${query.dataset_name})`);
|
|
372
399
|
}
|
|
373
|
-
console.log(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
400
|
+
console.log();
|
|
401
|
+
if (query.source === "dataset") {
|
|
402
|
+
if (query.columns && query.columns.length > 0) {
|
|
403
|
+
console.log(` Columns:`);
|
|
404
|
+
for (const col of query.columns) {
|
|
405
|
+
console.log(` ${col.name} (${col.type})`);
|
|
406
|
+
}
|
|
407
|
+
console.log();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (query.sql) {
|
|
411
|
+
console.log(` SQL:`);
|
|
412
|
+
console.log(` ${query.sql}\n`);
|
|
413
|
+
if (query.sample_row) {
|
|
414
|
+
console.log(` Sample row:`);
|
|
415
|
+
console.log(` ${JSON.stringify(query.sample_row, null, 2).split("\n").join("\n ")}\n`);
|
|
416
|
+
}
|
|
378
417
|
}
|
|
379
418
|
}
|
|
380
419
|
function resolveSql(sqlOrNext) {
|
|
@@ -391,10 +430,15 @@ function resolveSql(sqlOrNext) {
|
|
|
391
430
|
return sqlOrNext;
|
|
392
431
|
}
|
|
393
432
|
async function queryAdd(name, sql) {
|
|
433
|
+
// Check for --dataset flag
|
|
434
|
+
const datasetFlagIndex = process.argv.indexOf("--dataset");
|
|
435
|
+
const datasetName = datasetFlagIndex !== -1 ? process.argv[datasetFlagIndex + 1] : undefined;
|
|
394
436
|
const resolvedSql = resolveSql(sql);
|
|
395
|
-
if (!name || !resolvedSql) {
|
|
437
|
+
if (!name || (!resolvedSql && !datasetName)) {
|
|
396
438
|
console.error('\n Usage: npx @overscore/cli query add <name> "<sql>"\n');
|
|
397
|
-
console.error(" Or
|
|
439
|
+
console.error(" Or link an uploaded dataset:");
|
|
440
|
+
console.error(" npx @overscore/cli query add <name> --dataset <dataset-name>\n");
|
|
441
|
+
console.error(" Or with a SQL file:");
|
|
398
442
|
console.error(" npx @overscore/cli query add <name> --file query.sql\n");
|
|
399
443
|
process.exit(1);
|
|
400
444
|
}
|
|
@@ -403,7 +447,26 @@ async function queryAdd(name, sql) {
|
|
|
403
447
|
process.exit(1);
|
|
404
448
|
}
|
|
405
449
|
const { dashboardSlug } = loadEnv();
|
|
406
|
-
|
|
450
|
+
let body;
|
|
451
|
+
if (datasetName) {
|
|
452
|
+
// Look up the dataset UUID from project_datasets in the queries endpoint
|
|
453
|
+
const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
|
|
454
|
+
const dataset = (listData.project_datasets ?? []).find((ds) => ds.name === datasetName);
|
|
455
|
+
if (!dataset) {
|
|
456
|
+
console.error(`\n Error: Dataset "${datasetName}" not found in this project.\n`);
|
|
457
|
+
console.error(" Available datasets:");
|
|
458
|
+
for (const ds of listData.project_datasets ?? []) {
|
|
459
|
+
console.error(` - ${ds.name}`);
|
|
460
|
+
}
|
|
461
|
+
console.error();
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
body = { query_name: name, dataset_id: dataset.id };
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
body = { query_name: name, sql_text: resolvedSql };
|
|
468
|
+
}
|
|
469
|
+
const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body));
|
|
407
470
|
console.log(`\n Query "${data.query_name}" created.\n`);
|
|
408
471
|
console.log(` Use it in your dashboard:`);
|
|
409
472
|
console.log(` const { data } = useQuery("${data.query_name}");\n`);
|
|
@@ -435,24 +498,34 @@ async function queryRemove(name) {
|
|
|
435
498
|
await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
|
|
436
499
|
console.log(`\n Query "${name}" deleted.\n`);
|
|
437
500
|
}
|
|
438
|
-
function saveQueryResult(sql, data) {
|
|
501
|
+
function saveQueryResult(sql, data, sourceFile) {
|
|
439
502
|
try {
|
|
440
503
|
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
504
|
const dataDir = path.resolve(process.cwd(), "data");
|
|
505
|
+
// Check for ANY existing file with this hash — handles both old and new
|
|
506
|
+
// filename formats so the same query isn't duplicated.
|
|
507
|
+
if (fs.existsSync(dataDir)) {
|
|
508
|
+
const existing = fs.readdirSync(dataDir).find((f) => f.includes(hash) && f.endsWith(".json"));
|
|
509
|
+
if (existing) {
|
|
510
|
+
return { path: `data/${existing}`, cached: true };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Build filename: human-readable prefix when from a file, hash prefix otherwise
|
|
514
|
+
let filename;
|
|
515
|
+
if (sourceFile) {
|
|
516
|
+
const baseName = path.basename(sourceFile, path.extname(sourceFile));
|
|
517
|
+
filename = `${baseName}__${hash}.json`;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const snippet = sql
|
|
521
|
+
.toLowerCase()
|
|
522
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
523
|
+
.replace(/^_|_$/g, "")
|
|
524
|
+
.slice(0, 40);
|
|
525
|
+
filename = `${hash}_${snippet}.json`;
|
|
526
|
+
}
|
|
448
527
|
const fullPath = path.join(dataDir, filename);
|
|
449
528
|
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
529
|
if (!fs.existsSync(dataDir)) {
|
|
457
530
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
458
531
|
}
|
|
@@ -501,7 +574,9 @@ async function queryTest(sql) {
|
|
|
501
574
|
process.exit(1);
|
|
502
575
|
}
|
|
503
576
|
const data = (await apiRequest("POST", `/api/projects/${projectSlug}/test-query`, { sql: resolvedSql }));
|
|
504
|
-
const
|
|
577
|
+
const fileIndex = process.argv.indexOf("--file");
|
|
578
|
+
const sourceFile = fileIndex !== -1 ? process.argv[fileIndex + 1] : undefined;
|
|
579
|
+
const saved = saveQueryResult(resolvedSql, data, sourceFile);
|
|
505
580
|
const savedLine = saved
|
|
506
581
|
? saved.cached
|
|
507
582
|
? ` Cached (already saved earlier): ${saved.path}`
|
|
@@ -657,7 +732,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
|
657
732
|
if (dashCtxData.context_md) {
|
|
658
733
|
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
659
734
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
660
|
-
fs.writeFileSync(path.join(rulesDir, "dashboard
|
|
735
|
+
fs.writeFileSync(path.join(rulesDir, "this-dashboard.md"), dashCtxData.context_md);
|
|
661
736
|
console.log(" Updated dashboard context from Hub.");
|
|
662
737
|
}
|
|
663
738
|
}
|
|
@@ -889,19 +964,23 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
889
964
|
const authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
890
965
|
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
891
966
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
967
|
+
// Try knowledge base first — if the project has knowledge files, they
|
|
968
|
+
// replace the legacy company-context.md.
|
|
969
|
+
const hasKnowledge = await syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey);
|
|
970
|
+
// Company context — only fetch if no knowledge files exist (legacy path).
|
|
971
|
+
if (!hasKnowledge) {
|
|
972
|
+
try {
|
|
973
|
+
const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
|
|
974
|
+
if (ctxRes.ok) {
|
|
975
|
+
const ctxData = (await ctxRes.json());
|
|
976
|
+
if (ctxData.context_md) {
|
|
977
|
+
fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
|
|
978
|
+
}
|
|
900
979
|
}
|
|
901
980
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
981
|
+
catch {
|
|
982
|
+
// Non-fatal
|
|
983
|
+
}
|
|
905
984
|
}
|
|
906
985
|
// SQL style override — null means "use the CLI default"; leave the
|
|
907
986
|
// template/bundle file alone in that case.
|
|
@@ -918,6 +997,93 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
918
997
|
// Non-fatal
|
|
919
998
|
}
|
|
920
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Sync the project knowledge base from the Hub into .claude/knowledge/.
|
|
1002
|
+
* Returns true if knowledge files were found (caller can skip legacy
|
|
1003
|
+
* company-context.md in that case).
|
|
1004
|
+
*/
|
|
1005
|
+
async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
1006
|
+
const authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
1007
|
+
const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
|
|
1008
|
+
try {
|
|
1009
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, { headers: authHeader });
|
|
1010
|
+
if (!res.ok)
|
|
1011
|
+
return false;
|
|
1012
|
+
const data = (await res.json());
|
|
1013
|
+
if (!data.files || data.files.length === 0)
|
|
1014
|
+
return false;
|
|
1015
|
+
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1016
|
+
// Write each knowledge file
|
|
1017
|
+
for (const file of data.files) {
|
|
1018
|
+
fs.writeFileSync(path.join(knowledgeDir, file.path), file.content);
|
|
1019
|
+
}
|
|
1020
|
+
// Generate INDEX.md
|
|
1021
|
+
const indexLines = [
|
|
1022
|
+
"# Project Knowledge Base",
|
|
1023
|
+
"",
|
|
1024
|
+
"Read the relevant file when working on a topic. Do NOT read all files every session.",
|
|
1025
|
+
"",
|
|
1026
|
+
];
|
|
1027
|
+
for (const file of data.files) {
|
|
1028
|
+
indexLines.push(`- **${file.title}**: \`.claude/knowledge/${file.path}\``);
|
|
1029
|
+
}
|
|
1030
|
+
indexLines.push("");
|
|
1031
|
+
fs.writeFileSync(path.join(knowledgeDir, "INDEX.md"), indexLines.join("\n"));
|
|
1032
|
+
// Store hashes for conflict detection on upload
|
|
1033
|
+
const hashMap = {};
|
|
1034
|
+
for (const file of data.files) {
|
|
1035
|
+
hashMap[file.path] = file.content_hash;
|
|
1036
|
+
}
|
|
1037
|
+
fs.writeFileSync(path.join(knowledgeDir, ".knowledge-hashes"), JSON.stringify(hashMap, null, 2));
|
|
1038
|
+
console.log(` Knowledge base synced (${data.files.length} files).`);
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Upload local knowledge files back to the Hub.
|
|
1047
|
+
* Called from deploy/publish so knowledge learned during a session persists.
|
|
1048
|
+
*/
|
|
1049
|
+
async function uploadKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
|
|
1050
|
+
const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
|
|
1051
|
+
if (!fs.existsSync(knowledgeDir))
|
|
1052
|
+
return;
|
|
1053
|
+
const files = [];
|
|
1054
|
+
for (const entry of fs.readdirSync(knowledgeDir)) {
|
|
1055
|
+
// Skip INDEX.md (auto-generated) and dotfiles (.knowledge-hashes)
|
|
1056
|
+
if (entry === "INDEX.md" ||
|
|
1057
|
+
entry.startsWith(".") ||
|
|
1058
|
+
!entry.endsWith(".md"))
|
|
1059
|
+
continue;
|
|
1060
|
+
const content = fs.readFileSync(path.join(knowledgeDir, entry), "utf-8");
|
|
1061
|
+
// Extract title from first H1 line, fallback to filename
|
|
1062
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1063
|
+
const title = titleMatch
|
|
1064
|
+
? titleMatch[1]
|
|
1065
|
+
: entry.replace(/\.md$/, "").replace(/[-_]/g, " ");
|
|
1066
|
+
files.push({ path: entry, title, content });
|
|
1067
|
+
}
|
|
1068
|
+
if (files.length === 0)
|
|
1069
|
+
return;
|
|
1070
|
+
try {
|
|
1071
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, {
|
|
1072
|
+
method: "PUT",
|
|
1073
|
+
headers: {
|
|
1074
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
},
|
|
1077
|
+
body: JSON.stringify({ files }),
|
|
1078
|
+
});
|
|
1079
|
+
if (res.ok) {
|
|
1080
|
+
console.log(` Knowledge base synced (${files.length} files).`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
// Non-fatal
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
921
1087
|
/**
|
|
922
1088
|
* Fetch the latest platform-owned Claude Code rules from the Hub and write
|
|
923
1089
|
* them into the project's .claude/ directory. This keeps rules like
|
|
@@ -925,8 +1091,8 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
|
925
1091
|
* projects that were scaffolded months ago.
|
|
926
1092
|
*
|
|
927
1093
|
* Called from `deploy` (before uploading) and `pull` (after extracting).
|
|
928
|
-
* User-owned files (company-context.md, dashboard
|
|
929
|
-
* touched — those sync via separate mechanisms.
|
|
1094
|
+
* User-owned files (company-context.md, this-dashboard.md, knowledge/)
|
|
1095
|
+
* are NOT touched — those sync via separate mechanisms.
|
|
930
1096
|
*/
|
|
931
1097
|
async function syncPlatformRules(targetDir, baseUrl, apiKey) {
|
|
932
1098
|
try {
|
|
@@ -1273,6 +1439,8 @@ async function analysisPublish() {
|
|
|
1273
1439
|
console.log(`\n Published successfully!\n`);
|
|
1274
1440
|
printBundleSummary(files, process.cwd());
|
|
1275
1441
|
console.log(`\n URL: ${data.url}\n`);
|
|
1442
|
+
// Upload knowledge base so insights from this analysis persist
|
|
1443
|
+
await uploadKnowledgeBase(process.cwd(), baseUrl, meta.project_slug, cfg.apiKey);
|
|
1276
1444
|
}
|
|
1277
1445
|
/**
|
|
1278
1446
|
* Ensure analysis.json has a valid id by fetching from the Hub.
|
|
@@ -1844,7 +2012,8 @@ else if (command === "query") {
|
|
|
1844
2012
|
Commands:
|
|
1845
2013
|
list List all queries
|
|
1846
2014
|
show <name> Show a query's SQL and sample data
|
|
1847
|
-
add <name> "<sql>" Add a
|
|
2015
|
+
add <name> "<sql>" Add a SQL query
|
|
2016
|
+
add <name> --dataset <name> Link an uploaded dataset as a query
|
|
1848
2017
|
update <name> "<sql>" Update a query's SQL
|
|
1849
2018
|
remove <name> Delete a query
|
|
1850
2019
|
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.
|