@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. Tailor the message based on what kind of folder we're in so the
53
- // user gets pointed at the right setup command.
54
- const inAnalysisFolder = fs.existsSync(path.resolve(process.cwd(), ".overscore", "analysis.json"));
55
- if (inAnalysisFolder) {
56
- let analysisSlug = null;
57
- try {
58
- const meta = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".overscore", "analysis.json"), "utf-8"));
59
- analysisSlug = typeof meta.slug === "string" ? meta.slug : null;
60
- }
61
- catch {
62
- // Fall through to the generic message.
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
- else {
73
- console.error("\n Error: No credentials found. Run 'npx @overscore/cli auth login' to set up.\n");
74
- }
75
- process.exit(1);
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 company context (with conflict detection) legacy path for
272
- // projects that haven't migrated to the knowledge base yet.
273
- const companyCtxPath = path.resolve(process.cwd(), ".claude/rules/company-context.md");
274
- if (fs.existsSync(companyCtxPath)) {
275
- const content = fs.readFileSync(companyCtxPath, "utf-8");
276
- const hashPath = path.resolve(process.cwd(), ".claude/rules/.context-hash");
277
- const storedHash = fs.existsSync(hashPath) ? fs.readFileSync(hashPath, "utf-8").trim() : null;
278
- try {
279
- // Check if Hub version has changed since we pulled
280
- const checkRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, {
281
- headers: { Authorization: `Bearer ${apiKey}` },
282
- });
283
- const checkData = checkRes.ok ? (await checkRes.json()) : null;
284
- const hubHash = checkData?.context_md_hash || null;
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 (putRes.ok) {
300
- const putData = (await putRes.json());
301
- // Update stored hash
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
- catch {
311
- // Non-fatal
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
- // Fetch latest project context from Hub (may be newer than what's in the source zip)
740
- try {
741
- const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, {
742
- headers: { Authorization: `Bearer ${apiKey}` },
743
- });
744
- if (ctxRes.ok) {
745
- const ctxData = (await ctxRes.json());
746
- if (ctxData.context_md) {
747
- const rulesDir = path.join(targetDir, ".claude", "rules");
748
- fs.mkdirSync(rulesDir, { recursive: true });
749
- fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
750
- // Store hash for conflict detection on next deploy
751
- if (ctxData.context_md_hash) {
752
- fs.writeFileSync(path.join(rulesDir, ".context-hash"), ctxData.context_md_hash);
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
- catch {
759
- // Non-fatal the version from the source zip is fine
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
- console.log(" Updated dashboard context from Hub.");
741
+ dashboardContextLoaded = true;
773
742
  }
774
743
  }
775
744
  }
776
745
  catch {
777
746
  // Non-fatal
778
747
  }
779
- // Pull latest platform rules
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) Interactiveonly ask for the API key, resolve project automatically
927
- console.log("\n No Overscore config found. Let's set one up.\n");
928
- const apiKey = await prompt("API key (from the Hub):");
929
- if (!apiKey || !apiKey.startsWith("os_")) {
930
- console.error("\n Error: API key should start with os_\n");
931
- process.exit(1);
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
- // Look up the project slug from the key
934
- let projectSlug = null;
935
- try {
936
- const res = await fetch(`${HUB_URL}/api/projects/verify-key`, {
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: { "Content-Type": "application/json" },
939
- body: JSON.stringify({ key: apiKey }),
958
+ headers: {
959
+ Authorization: `Bearer ${deviceToken}`,
960
+ "Content-Type": "application/json",
961
+ },
962
+ body: JSON.stringify({ name: `cli-${Date.now()}` }),
940
963
  });
941
- if (res.ok) {
942
- const data = (await res.json());
943
- projectSlug = data.project_slug || null;
944
- if (projectSlug) {
945
- console.log(`\n Detected project: ${projectSlug}`);
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
- catch {
950
- // Fall back to manual entry
951
- }
952
- if (!projectSlug) {
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
- // Save for next time
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, `OVERSCORE_API_URL=${HUB_URL}/api\nOVERSCORE_API_KEY=${apiKey}\nOVERSCORE_PROJECT_SLUG=${projectSlug}\n`, { mode: 0o600 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
@@ -5,26 +5,32 @@ description: Reviews data insights discovered during dashboard or analysis work
5
5
 
6
6
  # Save Knowledge
7
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
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 dashboard code and any exploratory queries you ran.
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
- 2. Read `.claude/knowledge/INDEX.md` to see what's already documented.
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
- 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
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
- 4. Present proposed additions to the user for approval before writing any files.
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 dashboard-specific
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 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
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` to see what's already documented.
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
- 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
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 additions to the user for approval before writing any files.
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)