polymath-society 0.2.17 → 0.2.19

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/cli.js CHANGED
@@ -33,9 +33,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  ));
34
34
 
35
35
  // src/dataHome.ts
36
- import { existsSync } from "fs";
36
+ import { existsSync, readFileSync } from "fs";
37
37
  import os from "os";
38
38
  import path from "path";
39
+ function hasCompletedAnalysis(root) {
40
+ try {
41
+ const a = JSON.parse(readFileSync(path.join(root, "coding", "aggregate.json"), "utf-8"));
42
+ return !!String(a.bigPicture ?? "").trim();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
39
47
  function electronAppData() {
40
48
  const candidates = [
41
49
  "/Applications/Polymath.app/Contents/Resources/standalone/.data",
@@ -44,14 +52,18 @@ function electronAppData() {
44
52
  for (const c of candidates) if (existsSync(path.join(c, "coding"))) return c;
45
53
  return null;
46
54
  }
47
- function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData) {
55
+ function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData, home = os.homedir()) {
48
56
  const fromEnv = env.POLYMATH_DATA_DIR;
49
57
  if (fromEnv && fromEnv.trim()) return path.resolve(fromEnv);
50
58
  const local = path.join(cwd, ".data");
51
- if (existsSync(local)) return local;
59
+ const dflt = path.join(home, ".polymath-society", "data");
60
+ if (existsSync(local) && hasCompletedAnalysis(local)) return local;
52
61
  const fromApp = appData();
62
+ if (fromApp && hasCompletedAnalysis(fromApp)) return fromApp;
63
+ if (hasCompletedAnalysis(dflt)) return dflt;
64
+ if (existsSync(local)) return local;
53
65
  if (fromApp) return fromApp;
54
- return path.join(os.homedir(), ".polymath-society", "data");
66
+ return dflt;
55
67
  }
56
68
  var init_dataHome = __esm({
57
69
  "src/dataHome.ts"() {
@@ -2042,7 +2054,7 @@ __export(plan_exports, {
2042
2054
  tierFromStrings: () => tierFromStrings
2043
2055
  });
2044
2056
  import { promises as fs34 } from "fs";
2045
- import os9 from "os";
2057
+ import os10 from "os";
2046
2058
  import path39 from "path";
2047
2059
  function tierFromStrings(...hints) {
2048
2060
  for (const h of hints) {
@@ -2063,7 +2075,7 @@ function tierFromChatgptPlan(planType) {
2063
2075
  if (s === "free") return "gpt_free";
2064
2076
  return "unknown";
2065
2077
  }
2066
- async function detectCodexPlan(home = os9.homedir()) {
2078
+ async function detectCodexPlan(home = os10.homedir()) {
2067
2079
  try {
2068
2080
  const raw = JSON.parse(await fs34.readFile(path39.join(home, ".codex", "auth.json"), "utf8"));
2069
2081
  for (const tok of [raw.tokens?.id_token, raw.tokens?.access_token]) {
@@ -2079,7 +2091,7 @@ async function detectCodexPlan(home = os9.homedir()) {
2079
2091
  }
2080
2092
  return "unknown";
2081
2093
  }
2082
- async function detectPlan(home = os9.homedir()) {
2094
+ async function detectPlan(home = os10.homedir()) {
2083
2095
  let tier = "unknown";
2084
2096
  try {
2085
2097
  const raw = JSON.parse(await fs34.readFile(path39.join(home, ".claude.json"), "utf8"));
@@ -2173,7 +2185,7 @@ __export(wizard_exports, {
2173
2185
  sayExportLinks: () => sayExportLinks
2174
2186
  });
2175
2187
  import { promises as fs35 } from "fs";
2176
- import os10 from "os";
2188
+ import os11 from "os";
2177
2189
  import path40 from "path";
2178
2190
  import * as readline from "node:readline/promises";
2179
2191
  async function scanJsonlDir(label, dir) {
@@ -2313,7 +2325,7 @@ async function runWizard() {
2313
2325
  for (let i = 0; i < agentScans.length; i++) {
2314
2326
  const s = agentScans[i];
2315
2327
  const idx = present.indexOf(agentIds[i]);
2316
- say(s.files > 0 ? ` ${idx + 1}. ${green("\u2713")} ${bold(s.label)} \u2014 ${s.files.toLocaleString()} log files \xB7 newest ${s.newest} ${dim(s.dir.replace(os10.homedir(), "~"))}` : ` ${dim("\xB7")} ${dim(`${s.label} \u2014 none found`)} ${dim(s.dir.replace(os10.homedir(), "~"))}`);
2328
+ say(s.files > 0 ? ` ${idx + 1}. ${green("\u2713")} ${bold(s.label)} \u2014 ${s.files.toLocaleString()} log files \xB7 newest ${s.newest} ${dim(s.dir.replace(os11.homedir(), "~"))}` : ` ${dim("\xB7")} ${dim(`${s.label} \u2014 none found`)} ${dim(s.dir.replace(os11.homedir(), "~"))}`);
2317
2329
  }
2318
2330
  say(dim(" (log files include forks and agent runs \u2014 the report separates your real sessions out)"));
2319
2331
  if (present.length === 0) {
@@ -2376,9 +2388,9 @@ async function runWizard() {
2376
2388
  }
2377
2389
  };
2378
2390
  say(dim(" Scanning for known export formats (Spotlight + the usual folders, a few seconds)..."));
2379
- for (const c of await deterministicDiscover(os10.homedir())) add(await identifyPath(c));
2380
- for (const f of await scanForExports(path40.join(os10.homedir(), "Downloads"))) add(f);
2381
- for (const f of await scanForObsidianVaults(os10.homedir())) add(f);
2391
+ for (const c of await deterministicDiscover(os11.homedir())) add(await identifyPath(c));
2392
+ for (const f of await scanForExports(path40.join(os11.homedir(), "Downloads"))) add(f);
2393
+ for (const f of await scanForObsidianVaults(os11.homedir())) add(f);
2382
2394
  for (const f of [...previous?.exports.included ?? [], ...previous?.exports.excluded ?? []]) add(f);
2383
2395
  if (!found.length) {
2384
2396
  say(dim(" Nothing found in the usual places."));
@@ -2386,7 +2398,7 @@ async function runWizard() {
2386
2398
  try {
2387
2399
  const { invokeAgent: invokeAgent3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
2388
2400
  say(dim(" Searching (your agent writes its own filesystem searches; nothing leaves this machine; 90s max)..."));
2389
- const candidates = await agentDiscoverCandidates((inv) => invokeAgent3(inv), os10.homedir(), []);
2401
+ const candidates = await agentDiscoverCandidates((inv) => invokeAgent3(inv), os11.homedir(), []);
2390
2402
  for (const c of candidates) add(await identifyPath(c));
2391
2403
  } catch (e) {
2392
2404
  say(dim(` Agent search unavailable (${e instanceof Error ? e.message.slice(0, 80) : "failed"}).`));
@@ -2394,7 +2406,7 @@ async function runWizard() {
2394
2406
  }
2395
2407
  if (!found.length) {
2396
2408
  const folder = await ask(` Got them in a specific folder? ${dim("(path, or Enter to skip)")}: `);
2397
- if (folder) for (const f of await scanForExports(folder.replace(/^~(?=$|\/)/, os10.homedir()))) add(f);
2409
+ if (folder) for (const f of await scanForExports(folder.replace(/^~(?=$|\/)/, os11.homedir()))) add(f);
2398
2410
  }
2399
2411
  }
2400
2412
  if (found.length) {
@@ -2419,7 +2431,7 @@ async function runWizard() {
2419
2431
  approvedExports = manifest.exports.included;
2420
2432
  await writeManifest(dataDir, manifest);
2421
2433
  const nIn = manifest.exports.included.length;
2422
- say(` ${green("\u2713")} Saved to ${path40.join(dataDir, "exports-manifest.json").replace(os10.homedir(), "~")} \u2014 ${nIn} source${nIn === 1 ? "" : "s"} approved, ${manifest.exports.excluded.length} excluded (re-run anytime to change).`);
2434
+ say(` ${green("\u2713")} Saved to ${path40.join(dataDir, "exports-manifest.json").replace(os11.homedir(), "~")} \u2014 ${nIn} source${nIn === 1 ? "" : "s"} approved, ${manifest.exports.excluded.length} excluded (re-run anytime to change).`);
2423
2435
  for (const f of manifest.exports.excluded) say(` ${dim(`\u2717 ${path40.basename(f.path)} \u2014 excluded, never read past identification.`)}`);
2424
2436
  if (manifest.exports.included.some((f) => f.kind !== "unknown")) {
2425
2437
  say(` ${dim("These are ingested and analyzed locally when you pick the full analysis below.")}`);
@@ -2573,6 +2585,18 @@ function makeMultiBar(out = process.stderr) {
2573
2585
  if (m) l.within = { done: Number(m[1]), total: Number(m[2]) };
2574
2586
  }
2575
2587
  repaintAround(tty ? line : `[${name17}] ${line.trimStart()}`);
2588
+ },
2589
+ // Pin the lane at 100% immediately (up-to-date gate: the report is
2590
+ // ready NOW; remaining stages are silent housekeeping and must not
2591
+ // crawl the bar through partial percentages).
2592
+ finish(label) {
2593
+ const l = lanes.get(name17) ?? { i: 0, total: 0, label: "", within: null };
2594
+ l.total = l.total || 1;
2595
+ l.i = l.total + 1;
2596
+ l.label = label;
2597
+ l.within = null;
2598
+ lanes.set(name17, l);
2599
+ repaintAround(`\u25B8 ${name17} \u2713 ${label}`);
2576
2600
  }
2577
2601
  };
2578
2602
  },
@@ -4435,7 +4459,7 @@ import { promises as fs14 } from "fs";
4435
4459
  import path19 from "path";
4436
4460
 
4437
4461
  // ../../lib/agents/shared/agent.ts
4438
- import { readFileSync } from "fs";
4462
+ import { readFileSync as readFileSync2 } from "fs";
4439
4463
  import path13 from "path";
4440
4464
 
4441
4465
  // ../../lib/agents/shared/cliAdapter.ts
@@ -19883,7 +19907,7 @@ var ADAPTERS2 = {
19883
19907
  };
19884
19908
  function preferredEngine() {
19885
19909
  try {
19886
- const p = JSON.parse(readFileSync(path13.join(process.cwd(), ".data", "profile.json"), "utf-8"));
19910
+ const p = JSON.parse(readFileSync2(path13.join(process.cwd(), ".data", "profile.json"), "utf-8"));
19887
19911
  const e = p?.preferredEngine;
19888
19912
  return e === "cli" || e === "codex" || e === "cursor" || e === "api" ? e : null;
19889
19913
  } catch {
@@ -23054,6 +23078,8 @@ init_exportLinks();
23054
23078
  // src/publicCodingPageApi.ts
23055
23079
  import { promises as fs28 } from "fs";
23056
23080
  import path32 from "path";
23081
+ import os7 from "os";
23082
+ import crypto3 from "crypto";
23057
23083
 
23058
23084
  // ../../lib/agents/coding/publicPage.ts
23059
23085
  import { promises as fs27 } from "fs";
@@ -24453,6 +24479,49 @@ async function handleAnalysisShare(dataDir, kind, full) {
24453
24479
  return { status: 502, json: { error: e.message.slice(0, 200) } };
24454
24480
  }
24455
24481
  }
24482
+ function computeMachineUnion(machines) {
24483
+ const counts = /* @__PURE__ */ new Map();
24484
+ for (const m of Object.values(machines)) for (const s of m.sessions ?? []) counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
24485
+ return {
24486
+ machines: Object.entries(machines).map(([id, m]) => ({ id, label: m.label ?? id, sessions: (m.sessions ?? []).length, syncedAt: m.syncedAt ?? null })),
24487
+ uniqueSessions: counts.size,
24488
+ overlappingSessions: [...counts.values()].filter((n) => n > 1).length
24489
+ };
24490
+ }
24491
+ async function handleAnalysisSync(dataDir) {
24492
+ const cfg = centralConfig();
24493
+ if (!cfg.configured) return { status: 503, json: { error: "central services are disabled in this build" } };
24494
+ const auth = await getAccessToken(dataDir);
24495
+ if (!auth) return { status: 401, json: { error: "sign in with Google (top right) first" } };
24496
+ const mFile = path32.join(dataDir, "machine.json");
24497
+ let machine = await readJson4(mFile, null);
24498
+ if (!machine?.id) {
24499
+ machine = { id: crypto3.randomBytes(8).toString("hex"), label: os7.hostname().replace(/\.local$/, "") };
24500
+ await fs28.writeFile(mFile, JSON.stringify(machine, null, 2));
24501
+ }
24502
+ const rows = await readJson4(
24503
+ path32.join(dataDir, "coding", "sessions.json"),
24504
+ []
24505
+ );
24506
+ const sessions = rows.filter((r) => r.klass === "interactive" && !r.duplicateOf && r.sessionId).map((r) => ({ id: r.sessionId, start: r.start?.slice(0, 10) ?? null, chars: r.humanChars ?? 0, project: r.project ?? null, source: r.source ?? null }));
24507
+ if (!sessions.length) return { status: 409, json: { error: "nothing to sync yet \u2014 run the analysis first" } };
24508
+ const prior = await readOwnReportContent(cfg, auth.token, auth.identity.userId);
24509
+ try {
24510
+ const machines = prior.machines && typeof prior.machines === "object" ? prior.machines : {};
24511
+ machines[machine.id] = { label: machine.label, syncedAt: (/* @__PURE__ */ new Date()).toISOString(), sessions };
24512
+ const combined = computeMachineUnion(machines);
24513
+ const merged = { format: "coding-pr1", ...prior, machines, combined };
24514
+ const up = await fetch(`${cfg.supabaseUrl}/rest/v1/rpc/submit_report`, {
24515
+ method: "POST",
24516
+ headers: { apikey: cfg.supabaseAnonKey, authorization: `Bearer ${auth.token}`, "content-type": "application/json" },
24517
+ body: JSON.stringify({ p_session_id: null, p_content: JSON.stringify(merged) })
24518
+ });
24519
+ if (!up.ok) return { status: 502, json: { error: `sync failed (HTTP ${up.status})` } };
24520
+ return { status: 200, json: { ok: true, machine: machine.label, ...combined } };
24521
+ } catch (e) {
24522
+ return { status: 502, json: { error: e.message.slice(0, 200) } };
24523
+ }
24524
+ }
24456
24525
 
24457
24526
  // src/server.ts
24458
24527
  var __dirname = path33.dirname(fileURLToPath2(import.meta.url));
@@ -24780,6 +24849,11 @@ function startServer(opts) {
24780
24849
  json(res, out.status, out.json);
24781
24850
  return;
24782
24851
  }
24852
+ if (req.method === "POST" && route === "/api/analysis/sync") {
24853
+ const out = await handleAnalysisSync(dataDir);
24854
+ json(res, out.status, out.json);
24855
+ return;
24856
+ }
24783
24857
  if (req.method === "POST" && route === "/api/analysis/share") {
24784
24858
  const body = JSON.parse(await readBody(req) || "{}");
24785
24859
  const out = await handleAnalysisShare(dataDir, body.kind === "chat" ? "chat" : "coding", {
@@ -24843,7 +24917,7 @@ function startServer(opts) {
24843
24917
 
24844
24918
  // src/pipeline.ts
24845
24919
  import { spawn as spawn7 } from "child_process";
24846
- import os8 from "os";
24920
+ import os9 from "os";
24847
24921
  import path36 from "path";
24848
24922
  import { existsSync as existsSync5, promises as fs31 } from "fs";
24849
24923
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -24855,10 +24929,10 @@ import { promises as fs30 } from "fs";
24855
24929
  import path35 from "path";
24856
24930
 
24857
24931
  // ../../lib/agents/shared/paths.ts
24858
- import os7 from "os";
24932
+ import os8 from "os";
24859
24933
  import path34 from "path";
24860
24934
  function polymathHome() {
24861
- return process.env.POLYMATH_HOME || path34.join(os7.homedir(), ".polymath-society");
24935
+ return process.env.POLYMATH_HOME || path34.join(os8.homedir(), ".polymath-society");
24862
24936
  }
24863
24937
  function agentDir(agent) {
24864
24938
  return path34.join(polymathHome(), agent);
@@ -25017,7 +25091,7 @@ async function startRunLog(clientInfo = {}, opts = {}) {
25017
25091
 
25018
25092
  // src/pipeline.ts
25019
25093
  var MODULE_DIR2 = path36.dirname(fileURLToPath3(import.meta.url));
25020
- function machineBudget(totalMemBytes = os8.totalmem(), cpus = os8.availableParallelism?.() ?? os8.cpus().length, envOverride = process.env.POLYMATH_CONC) {
25094
+ function machineBudget(totalMemBytes = os9.totalmem(), cpus = os9.availableParallelism?.() ?? os9.cpus().length, envOverride = process.env.POLYMATH_CONC) {
25021
25095
  const envB = Number(envOverride);
25022
25096
  if (Number.isFinite(envB) && envB >= 1) return Math.min(32, Math.floor(envB));
25023
25097
  const byMem = Math.floor(totalMemBytes / 2 ** 30 * 0.35 / 0.45);
@@ -25262,6 +25336,7 @@ async function runPipeline(o) {
25262
25336
  const gate2 = await evalUpToDate(codingDir, threshold, o.regrade);
25263
25337
  if (gate2.skip) {
25264
25338
  skippedLlm = { pending: gate2.pending, threshold };
25339
+ o.onUpToDate?.();
25265
25340
  o.onLine?.(
25266
25341
  `\u2713 report is up to date \u2014 ${gate2.pending} new session(s) since the last full run (below the ${threshold}-session threshold). Skipping the AI stages; free metrics still refresh. They'll be analyzed once ${threshold}+ accumulate (POLYMATH_MIN_NEW_SESSIONS=0 or --regrade to force now).`
25267
25342
  );
@@ -25465,6 +25540,7 @@ async function runChatPipeline(o) {
25465
25540
  const report = await fs32.readFile(path37.join(dataRoot, "engine-pilot", "public-report.json"), "utf8").then((s) => JSON.parse(s)).catch(() => null);
25466
25541
  if (report?.headline && (report.spikes?.length ?? 0) > 0 && newDocs < minNewDocs) {
25467
25542
  upToDate = true;
25543
+ o.onUpToDate?.();
25468
25544
  o.onLine?.(`\u2713 chat report is up to date \u2014 ${newDocs} new doc(s) since the last full run (below the ${minNewDocs}-doc threshold), so the AI stages are skipped and the existing report is served as-is. POLYMATH_MIN_NEW_DOCS=0 forces a full run.`);
25469
25545
  }
25470
25546
  }
@@ -25578,6 +25654,18 @@ ALL analysis artifacts land under ONE data home, printed at startup:
25578
25654
  POLYMATH_DATA_DIR if set, else ./.data when it already exists, else
25579
25655
  ~/.polymath-society/data. Re-run anywhere \u2014 it resumes from that same folder.`;
25580
25656
  var round13 = (x) => Math.round(x * 10) / 10;
25657
+ async function readPriorAnalysis() {
25658
+ try {
25659
+ const agg = JSON.parse(await fs36.readFile(path41.join(DATA_ROOT, "coding", "aggregate.json"), "utf-8"));
25660
+ if (!agg.bigPicture || !String(agg.bigPicture).trim()) return null;
25661
+ const graded = (await fs36.readdir(path41.join(DATA_ROOT, "coding", "grades")).catch(() => [])).filter((f) => f.endsWith(".json")).length;
25662
+ const stat = await fs36.stat(path41.join(DATA_ROOT, "coding", "aggregate.json"));
25663
+ const when = (agg.generatedAt ?? stat.mtime.toISOString()).slice(0, 10);
25664
+ return { graded, when };
25665
+ } catch {
25666
+ return null;
25667
+ }
25668
+ }
25581
25669
  function printProfileSummary(p) {
25582
25670
  const t = p.throughput;
25583
25671
  const log = console.log;
@@ -25648,14 +25736,27 @@ async function main() {
25648
25736
  await maybePrintUpdateNotice(opts);
25649
25737
  const wantWizard = process.env.POLYMATH_WIZARD === "1" || process.env.POLYMATH_WIZARD !== "0" && process.argv.length <= 2 && !!process.stdin.isTTY && !!process.stdout.isTTY;
25650
25738
  if (wantWizard) {
25651
- const { runWizard: runWizard2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
25652
- const w = await runWizard2();
25653
- if (!w) return;
25654
- opts.cmd = w.cmd;
25655
- opts.grade = w.grade;
25656
- opts.overnight = w.overnight;
25657
- opts.chatOvernight = w.chatOvernight;
25658
- opts.open = w.open;
25739
+ console.error(" Checking for existing analysis\u2026");
25740
+ const prior = process.env.POLYMATH_WIZARD === "1" ? null : await readPriorAnalysis();
25741
+ if (prior) {
25742
+ console.error(` Found your finished report \u2014 ${prior.graded} graded sessions, last updated ${prior.when}.`);
25743
+ console.error(` Serving it now. The progress bars below are just a quick check for anything new \u2014`);
25744
+ console.error(` your finished analysis is never redone.`);
25745
+ console.error(` To change sources or redo setup: POLYMATH_WIZARD=1 polymath-society
25746
+ `);
25747
+ opts.cmd = "serve";
25748
+ opts.grade = true;
25749
+ opts.open = true;
25750
+ } else {
25751
+ const { runWizard: runWizard2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
25752
+ const w = await runWizard2();
25753
+ if (!w) return;
25754
+ opts.cmd = w.cmd;
25755
+ opts.grade = w.grade;
25756
+ opts.overnight = w.overnight;
25757
+ opts.chatOvernight = w.chatOvernight;
25758
+ opts.open = w.open;
25759
+ }
25659
25760
  }
25660
25761
  if (opts.cmd === "grade") {
25661
25762
  const backend = ensureBackend(opts.adapter);
@@ -25756,6 +25857,7 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
25756
25857
  };
25757
25858
  mbar.setLink(earlyHandle.url);
25758
25859
  const codingLane = mbar.lane("coding");
25860
+ let codingPinned = false;
25759
25861
  const codingDone = runPipeline({
25760
25862
  repoRoot: REPO_ROOT,
25761
25863
  model: opts.model,
@@ -25764,7 +25866,14 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
25764
25866
  codex: opts.codex,
25765
25867
  idleGapMin: opts.idleGapMin,
25766
25868
  regrade: opts.regrade,
25869
+ onUpToDate: () => {
25870
+ codingPinned = true;
25871
+ progress.coding = { i: 2, total: 1, label: "everything up to date", done: false };
25872
+ earlyHandle.setProgress(progress);
25873
+ codingLane.finish("everything up to date");
25874
+ },
25767
25875
  onStage: (i, total, label, llm) => {
25876
+ if (codingPinned) return;
25768
25877
  progress.coding = { ...progress.coding, i, total, label, done: false };
25769
25878
  earlyHandle.setProgress(progress);
25770
25879
  codingLane.stage(i, total, `${label}${llm ? " (AI)" : ""}`);
@@ -25802,11 +25911,19 @@ Running the full analysis on ${backend.name === "cli" ? "Claude Code" : "Codex"}
25802
25911
  return null;
25803
25912
  }
25804
25913
  chatLane.log(` chat analysis starting over your ${manifest.exports.included.length} approved source(s) \u2014 same engine as the hosted app.`);
25914
+ let chatPinned = false;
25805
25915
  const chat2 = await runChatPipeline({
25806
25916
  repoRoot: REPO_ROOT,
25807
25917
  model: opts.model,
25808
25918
  overnight: chatOvernight,
25919
+ onUpToDate: () => {
25920
+ chatPinned = true;
25921
+ progress.chat = { i: 2, total: 1, label: "everything up to date", done: false };
25922
+ earlyHandle.setProgress(progress);
25923
+ chatLane.finish("everything up to date");
25924
+ },
25809
25925
  onStage: (i, total, label, llm) => {
25926
+ if (chatPinned) return;
25810
25927
  progress.chat = { ...progress.chat, i, total, label, done: false };
25811
25928
  earlyHandle.setProgress(progress);
25812
25929
  chatLane.stage(i, total, `${label}${llm ? " (AI)" : ""}`);
package/dist/index.js CHANGED
@@ -21785,9 +21785,17 @@ Apply the user's LAST instruction and return the complete updated payload.`;
21785
21785
  }
21786
21786
 
21787
21787
  // src/dataHome.ts
21788
- import { existsSync as existsSync4 } from "fs";
21788
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
21789
21789
  import os6 from "os";
21790
21790
  import path26 from "path";
21791
+ function hasCompletedAnalysis(root) {
21792
+ try {
21793
+ const a = JSON.parse(readFileSync2(path26.join(root, "coding", "aggregate.json"), "utf-8"));
21794
+ return !!String(a.bigPicture ?? "").trim();
21795
+ } catch {
21796
+ return false;
21797
+ }
21798
+ }
21791
21799
  function electronAppData() {
21792
21800
  const candidates = [
21793
21801
  "/Applications/Polymath.app/Contents/Resources/standalone/.data",
@@ -21796,14 +21804,18 @@ function electronAppData() {
21796
21804
  for (const c of candidates) if (existsSync4(path26.join(c, "coding"))) return c;
21797
21805
  return null;
21798
21806
  }
21799
- function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData) {
21807
+ function resolveDataRoot(env = process.env, cwd = process.cwd(), appData = electronAppData, home = os6.homedir()) {
21800
21808
  const fromEnv = env.POLYMATH_DATA_DIR;
21801
21809
  if (fromEnv && fromEnv.trim()) return path26.resolve(fromEnv);
21802
21810
  const local = path26.join(cwd, ".data");
21803
- if (existsSync4(local)) return local;
21811
+ const dflt = path26.join(home, ".polymath-society", "data");
21812
+ if (existsSync4(local) && hasCompletedAnalysis(local)) return local;
21804
21813
  const fromApp = appData();
21814
+ if (fromApp && hasCompletedAnalysis(fromApp)) return fromApp;
21815
+ if (hasCompletedAnalysis(dflt)) return dflt;
21816
+ if (existsSync4(local)) return local;
21805
21817
  if (fromApp) return fromApp;
21806
- return path26.join(os6.homedir(), ".polymath-society", "data");
21818
+ return dflt;
21807
21819
  }
21808
21820
 
21809
21821
  // src/manifest.ts
@@ -21879,6 +21891,8 @@ var EXPORT_LINKS = [
21879
21891
  // src/publicCodingPageApi.ts
21880
21892
  import { promises as fs28 } from "fs";
21881
21893
  import path32 from "path";
21894
+ import os7 from "os";
21895
+ import crypto3 from "crypto";
21882
21896
 
21883
21897
  // ../../lib/agents/coding/publicPage.ts
21884
21898
  import { promises as fs27 } from "fs";
@@ -23278,6 +23292,49 @@ async function handleAnalysisShare(dataDir, kind, full) {
23278
23292
  return { status: 502, json: { error: e.message.slice(0, 200) } };
23279
23293
  }
23280
23294
  }
23295
+ function computeMachineUnion(machines) {
23296
+ const counts = /* @__PURE__ */ new Map();
23297
+ for (const m of Object.values(machines)) for (const s of m.sessions ?? []) counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
23298
+ return {
23299
+ machines: Object.entries(machines).map(([id, m]) => ({ id, label: m.label ?? id, sessions: (m.sessions ?? []).length, syncedAt: m.syncedAt ?? null })),
23300
+ uniqueSessions: counts.size,
23301
+ overlappingSessions: [...counts.values()].filter((n) => n > 1).length
23302
+ };
23303
+ }
23304
+ async function handleAnalysisSync(dataDir) {
23305
+ const cfg = centralConfig();
23306
+ if (!cfg.configured) return { status: 503, json: { error: "central services are disabled in this build" } };
23307
+ const auth = await getAccessToken(dataDir);
23308
+ if (!auth) return { status: 401, json: { error: "sign in with Google (top right) first" } };
23309
+ const mFile = path32.join(dataDir, "machine.json");
23310
+ let machine = await readJson4(mFile, null);
23311
+ if (!machine?.id) {
23312
+ machine = { id: crypto3.randomBytes(8).toString("hex"), label: os7.hostname().replace(/\.local$/, "") };
23313
+ await fs28.writeFile(mFile, JSON.stringify(machine, null, 2));
23314
+ }
23315
+ const rows = await readJson4(
23316
+ path32.join(dataDir, "coding", "sessions.json"),
23317
+ []
23318
+ );
23319
+ const sessions = rows.filter((r) => r.klass === "interactive" && !r.duplicateOf && r.sessionId).map((r) => ({ id: r.sessionId, start: r.start?.slice(0, 10) ?? null, chars: r.humanChars ?? 0, project: r.project ?? null, source: r.source ?? null }));
23320
+ if (!sessions.length) return { status: 409, json: { error: "nothing to sync yet \u2014 run the analysis first" } };
23321
+ const prior = await readOwnReportContent(cfg, auth.token, auth.identity.userId);
23322
+ try {
23323
+ const machines = prior.machines && typeof prior.machines === "object" ? prior.machines : {};
23324
+ machines[machine.id] = { label: machine.label, syncedAt: (/* @__PURE__ */ new Date()).toISOString(), sessions };
23325
+ const combined = computeMachineUnion(machines);
23326
+ const merged = { format: "coding-pr1", ...prior, machines, combined };
23327
+ const up = await fetch(`${cfg.supabaseUrl}/rest/v1/rpc/submit_report`, {
23328
+ method: "POST",
23329
+ headers: { apikey: cfg.supabaseAnonKey, authorization: `Bearer ${auth.token}`, "content-type": "application/json" },
23330
+ body: JSON.stringify({ p_session_id: null, p_content: JSON.stringify(merged) })
23331
+ });
23332
+ if (!up.ok) return { status: 502, json: { error: `sync failed (HTTP ${up.status})` } };
23333
+ return { status: 200, json: { ok: true, machine: machine.label, ...combined } };
23334
+ } catch (e) {
23335
+ return { status: 502, json: { error: e.message.slice(0, 200) } };
23336
+ }
23337
+ }
23281
23338
 
23282
23339
  // src/server.ts
23283
23340
  var __dirname = path33.dirname(fileURLToPath2(import.meta.url));
@@ -23605,6 +23662,11 @@ function startServer(opts) {
23605
23662
  json(res, out.status, out.json);
23606
23663
  return;
23607
23664
  }
23665
+ if (req.method === "POST" && route === "/api/analysis/sync") {
23666
+ const out = await handleAnalysisSync(dataDir);
23667
+ json(res, out.status, out.json);
23668
+ return;
23669
+ }
23608
23670
  if (req.method === "POST" && route === "/api/analysis/share") {
23609
23671
  const body = JSON.parse(await readBody(req) || "{}");
23610
23672
  const out = await handleAnalysisShare(dataDir, body.kind === "chat" ? "chat" : "coding", {
@@ -17191,6 +17191,9 @@ async function main() {
17191
17191
  return m ? `--- "${s.title}" \xB7 ${(s.start || "").slice(0, 10)} ---
17192
17192
  ${m.text.slice(0, 3e3)}` : "";
17193
17193
  }))).filter(Boolean);
17194
+ const digestDays = (await fs11.readFile(path13.join(CODING, "day-digest.json"), "utf-8").then(JSON.parse).catch(() => ({ days: [] }))).days ?? [];
17195
+ const digestLines = digestDays.slice().sort((a, b) => a.date < b.date ? -1 : 1).slice(-21).map((d) => `${d.date}: ${d.overall ?? ""}
17196
+ ${(d.conversations ?? []).slice(0, 4).map((c) => ` - ${c.title ?? ""}: ${c.summary ?? ""}`).join("\n")}`).join("\n").slice(0, 12e3);
17194
17197
  const SYS = "You are a Claude Code power-user coach. You judge ONE person against the COMPLETE catalog of best ways to use Claude Code (the rubric below, sourced from Boris Cherny + first-party Claude Code features) and write a comprehensive, honest gap analysis, ORDERED most-difference-making \u2192 least. Rules: (1) Go through the whole catalog and surface the techniques they are NOT doing (or under-using) that would genuinely help \u2014 respecting each tip's [EMPHASIZE]/[DE-EMPHASIZE]/[GATE] curation. (2) ORDER by differenceRank (1 = biggest unlock for THIS person). Follow the rubric's PRIME DIRECTIVE: if usage.parallelism.alreadyParallel is FALSE \u2192 'Do more in parallel' is rank 1; else \u2192 'Invest in your CLAUDE.md / tune your context' is rank 1. Then Plan mode, then 'Give Claude test cases so it can auto-verify and run longer' (Boris's 'most important thing' \u2014 rank it high, right after plan mode), then subagents, and the rest per the rubric order, skipping any that don't apply. (3) GATE the conditional tips on the STRUCTURAL usage facts, NOT on keywords \u2014 and a tip whose gate FAILS is OMITTED from recommendations entirely, never included as a softened/conditional card (recommending something their data says doesn't apply, or that they already do, reads as not having looked): loops require real PR/branch volume in usage.collaboration, else OMIT; the Chrome tip requires ~zero browser-driving in usage.chrome, else OMIT and credit in alreadyStrong; forking uses the RECENT count in usage.forking \u2014 if they fork in the recent window they already do it, OMIT and credit; remote/mobile/EC2 fires only when usage.surface.workstationBound is TRUE; plan mode by usage.planMode; subagents by usage.subagents. If they already do a thing, it goes in alreadyStrong, NOT in recommendations. (4) \u26D4 CRITICAL \u2014 this person BUILDS Claude Code tooling, so their transcripts are full of the words 'worktree', 'pull request', 'fork', 'plan mode', 'by the way', 'teleport'. A keyword in the transcript is NOT evidence they use the feature. Rely on the STRUCTURAL facts only. (5) Ground every recommendation in a RECEIPT \u2014 a structural fact from their usage or a concrete session pain point \u2014 and CONVINCE with a specific case for how it helps THIS person, not a generic benefit. (6) Tailor every action to the Claude Desktop Mac app; NEVER give terminal-only `claude -p` advice. (7) Honor config.deliberateChoices (they stay on `main` on purpose \u2014 do NOT tell them to hand-juggle worktrees; parallel SESSIONS instead) and config.claudeAutoTools (never tell them to 'use' a tool Claude drives itself; for 'explain with visuals' the user just ASKS for a visual \u2014 do not name the tool). (8) Also name what they ALREADY do well. (9) SELF-CONTAINED but not verbose \u2014 lead with the PROBLEM, then the SOLUTION, then HOW to use it in their context. Plain language, no jargon, no enumerating multiple dates. An essay-like rec is a failed rec; so is a cryptic one. Follow this writing rubric strictly:\n\n" + writing + sourceLookupNote();
17195
17198
  const prompt = `THE RUBRIC \u2014 the complete catalog of best ways to use Claude Code. Judge the person against ALL of it, and ORDER by differenceRank per its PRIME DIRECTIVE:
17196
17199
 
@@ -17200,6 +17203,9 @@ ${techniques}
17200
17203
  WHAT THIS PERSON ACTUALLY DOES (STRUCTURAL usage facts + config \u2014 ground truth; trust these over anything in the transcripts):
17201
17204
  ${JSON.stringify(usage, null, 1)}
17202
17205
 
17206
+ THEIR RECENT DAYS, task by task (dated ground material \u2014 draw applyItHere moments from HERE and from the excerpts, never from imagination):
17207
+ ${digestLines}
17208
+
17203
17209
  SESSION EXCERPTS (for pain points / friction / grounding ONLY \u2014 never as proof they USE a feature; ">> " = the person, "[AI]" = the model):
17204
17210
  ${excerpts.join("\n\n").slice(0, 6e4)}
17205
17211
 
@@ -17207,11 +17213,12 @@ ${excerpts.join("\n\n").slice(0, 6e4)}
17207
17213
  Produce the gap analysis. Be thorough: cover every relevant catalog item they're missing, ordered most-difference-making first. Output a SINGLE fenced json block (every string a complete, punctuated, no-fluff sentence):
17208
17214
  \`\`\`json
17209
17215
  {
17210
- "recommendations": [ { "title": "Boris's OWN / the feature's general wording for the tip (e.g. 'Invest in your CLAUDE.md', 'Start every complex task in Plan mode', 'Give Claude test cases so it can auto-verify and run longer', 'Code from anywhere with a remote instance', 'Fork your session', 'Use /btw for side queries') \u2014 NOT a hyper-specific name. For the verification tip the title MUST contain 'verify its own work' or 'check its own work' so the report picks the right icon.", "category": "rubric tip", "status": "not-doing|under-using", "differenceRank": 1, "gap": "LEAD WITH THE PROBLEM in plain language \u2014 what it's currently costing them, grounded in a STRUCTURAL fact. One clear, self-contained sentence (~12-18 words), no jargon. If they already do it somewhat, frame it as 'you do X; the gap is pushing it further'.", "whyItHelps": "The SOLUTION, then HOW to use it in THEIR context with a concrete example. Plain language, self-contained, ~30-45 words. Two crisp beats: fix, then their-context application. HARD RULE: no jargon, and NEVER name a Claude-auto tool. EXAMPLE (plan mode): 'Skipping the plan makes Claude pause to ask and drift. Fix: iterate the whole approach in Plan mode first, and drop in concrete expected results as test cases so Claude self-verifies and runs autonomously far longer.'", "payoff": 5 } ],
17216
+ "recommendations": [ { "title": "Boris's OWN / the feature's general wording for the tip (e.g. 'Invest in your CLAUDE.md', 'Start every complex task in Plan mode', 'Give Claude test cases so it can auto-verify and run longer', 'Code from anywhere with a remote instance', 'Fork your session', 'Use /btw for side queries') \u2014 NOT a hyper-specific name. For the verification tip the title MUST contain 'verify its own work' or 'check its own work' so the report picks the right icon.", "category": "rubric tip", "status": "not-doing|under-using", "differenceRank": 1, "gap": "LEAD WITH THE PROBLEM in plain language \u2014 what it's currently costing them, grounded in a STRUCTURAL fact. One clear, self-contained sentence (~12-18 words), no jargon. If they already do it somewhat, frame it as 'you do X; the gap is pushing it further'.", "whyItHelps": "The SOLUTION, then HOW to use it in THEIR context with a concrete example. Plain language, self-contained, ~30-45 words. Two crisp beats: fix, then their-context application. HARD RULE: no jargon, and NEVER name a Claude-auto tool. EXAMPLE (plan mode): 'Skipping the plan makes Claude pause to ask and drift. Fix: iterate the whole approach in Plan mode first, and drop in concrete expected results as test cases so Claude self-verifies and runs autonomously far longer.'", "payoff": 5, "applyItHere": [ { "moment": "A SPECIFIC dated moment from THEIR OWN days/excerpts \u2014 name the repo/project and the actual task in their vocabulary (~15-25 words).", "insteadDo": "How this exact technique changes that exact moment \u2014 concrete, imperative, their context (~15-25 words)." } ] } ],
17211
17217
  "alreadyStrong": [ { "title": "", "note": "What they already do well, concretely." } ]
17212
17218
  }
17213
17219
  \`\`\`
17214
- \`differenceRank\` is a strict 1..N ordering (1 = highest). Assign it per the PRIME DIRECTIVE and the rubric's general order; the report renders recommendations sorted by it. \`payoff\` (1-5) is a secondary strength cue. Recommend ONLY from the catalog.`;
17220
+ \`differenceRank\` is a strict 1..N ordering (1 = highest). Assign it per the PRIME DIRECTIVE and the rubric's general order; the report renders recommendations sorted by it. \`payoff\` (1-5) is a secondary strength cue. Recommend ONLY from the catalog.
17221
+ \`applyItHere\` HARD RULES: 3 entries per recommendation when the material supports 3, minimum 2 \u2014 and every moment must be VISIBLY present in THEIR RECENT DAYS or the excerpts (a real dated task, named the way they name it). NEVER invent, generalize, or reuse the same moment across recommendations. If the material truly supports only one real moment, give that one \u2014 a fabricated example is worse than a short list.`;
17215
17222
  const sha = promptSha([SYS, prompt, "sonnet"]);
17216
17223
  if (!process.argv.includes("--refresh")) {
17217
17224
  const prev = await reusableOutput(path13.join(CODING, "gap.json"), sha, (o) => Array.isArray(o.recommendations) && o.recommendations.length > 0);
package/dist/web/app.js CHANGED
@@ -7291,6 +7291,8 @@ var import_jsx_runtime = __toESM(require_jsx_runtime());
7291
7291
  function ShareAnalysisRow({ kind, label }) {
7292
7292
  const [state, setState] = (0, import_react.useState)("idle");
7293
7293
  const [err, setErr] = (0, import_react.useState)("");
7294
+ const [sync, setSync] = (0, import_react.useState)("idle");
7295
+ const [syncMsg, setSyncMsg] = (0, import_react.useState)("");
7294
7296
  const send = async () => {
7295
7297
  setState("sending");
7296
7298
  try {
@@ -7309,23 +7311,55 @@ function ShareAnalysisRow({ kind, label }) {
7309
7311
  setState("error");
7310
7312
  }
7311
7313
  };
7314
+ const doSync = async () => {
7315
+ setSync("sending");
7316
+ setSyncMsg("");
7317
+ try {
7318
+ const r = await fetch("/api/analysis/sync", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }).then((r2) => r2.json());
7319
+ if (r.ok) {
7320
+ const n = r.machines?.length ?? 1;
7321
+ const overlap = r.overlappingSessions ? ` \xB7 ${r.overlappingSessions} overlapping, counted once` : "";
7322
+ setSyncMsg(`Synced \u2713 ${r.machine} \u2014 ${r.uniqueSessions.toLocaleString()} unique sessions across ${n} machine${n === 1 ? "" : "s"}${overlap}`);
7323
+ setSync("idle");
7324
+ } else {
7325
+ setSyncMsg(r.error || "couldn't sync");
7326
+ setSync("error");
7327
+ }
7328
+ } catch {
7329
+ setSyncMsg("couldn't sync \u2014 try again");
7330
+ setSync("error");
7331
+ }
7332
+ };
7312
7333
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5 rounded-xl border border-line bg-paper px-3.5 py-2.5", children: [
7313
7334
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-[12px] text-muted", children: [
7314
7335
  "Optional: share this full ",
7315
7336
  label,
7316
- " as feedback for Polymath (will greatly help us make this better!). Private \u2014 nothing gets a public link."
7337
+ " as feedback for Polymath (will greatly help us make this better!). Private \u2014 nothing gets a public link.",
7338
+ kind === "coding" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mt-1 block", children: "Work on two machines? Sync each to your account and they combine, overlap counted once." })
7317
7339
  ] }),
7318
- state === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "shrink-0 text-[12px] font-medium text-ink", children: "Shared \u2713" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex shrink-0 items-center gap-2", children: [
7319
- state === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[11px] text-amber-700", children: err }),
7320
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex shrink-0 items-center gap-2", children: [
7341
+ syncMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `text-[11px] ${sync === "error" ? "text-amber-700" : "text-ink"}`, children: syncMsg }),
7342
+ kind === "coding" && !syncMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7321
7343
  "button",
7322
7344
  {
7323
- onClick: send,
7324
- disabled: state === "sending",
7345
+ onClick: doSync,
7346
+ disabled: sync === "sending",
7325
7347
  className: "rounded-full border border-line px-3 py-1 text-[12px] text-ink transition-colors hover:border-accent disabled:opacity-50",
7326
- children: state === "sending" ? "Sending\u2026" : "Share it"
7348
+ children: sync === "sending" ? "Syncing\u2026" : "Sync to account"
7327
7349
  }
7328
- )
7350
+ ),
7351
+ state === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[12px] font-medium text-ink", children: "Shared \u2713" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
7352
+ state === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[11px] text-amber-700", children: err }),
7353
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
7354
+ "button",
7355
+ {
7356
+ onClick: send,
7357
+ disabled: state === "sending",
7358
+ className: "rounded-full border border-line px-3 py-1 text-[12px] text-ink transition-colors hover:border-accent disabled:opacity-50",
7359
+ children: state === "sending" ? "Sending\u2026" : "Share it"
7360
+ }
7361
+ )
7362
+ ] })
7329
7363
  ] })
7330
7364
  ] });
7331
7365
  }
@@ -9640,7 +9674,14 @@ function FrontierPanel({ f, detail, coaching, gap, parallelism }) {
9640
9674
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-1 text-[12.5px] leading-relaxed text-ink", children: [
9641
9675
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "font-semibold text-accent", children: "Why it helps you \u2014 " }),
9642
9676
  r.whyItHelps
9643
- ] })
9677
+ ] }),
9678
+ r.applyItHere?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-2 border-t border-line pt-2", children: [
9679
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-[11px] font-semibold uppercase tracking-wider text-faint", children: "Where this would have hit, in your own work" }),
9680
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "mt-1.5 space-y-2", children: r.applyItHere.map((ex, j) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "leading-snug", children: [
9681
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-[12px] font-semibold text-ink", children: ex.insteadDo }),
9682
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-0.5 text-[11.5px] text-ink/80", children: ex.moment })
9683
+ ] }, j)) })
9684
+ ] }) : null
9644
9685
  ] }),
9645
9686
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "hidden flex-none items-center self-stretch sm:flex", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TipVisual, { title: r.title }) })
9646
9687
  ] }) }, i)) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymath-society",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Deterministic metrics from your local Claude Code / Codex agent logs — parallelism, flow, throughput, projects. Zero LLM, zero server, on-device.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "build:ts": "node scripts/bundle.mjs",
41
41
  "build:ui": "node scripts/build-ui.mjs",
42
42
  "clean": "rm -rf dist",
43
- "prepublishOnly": "npm run clean && npm run build",
43
+ "prepublishOnly": "node scripts/check-publish-version.mjs && npm run clean && npm run build",
44
44
  "start": "node dist/cli.js",
45
45
  "serve": "node dist/cli.js serve",
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit",