qodewk 0.2.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.cjs +314 -132
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -114,10 +114,14 @@ Unlike single-vendor utilities (like Chit, which only support Claude Code CLI),
114
114
  | `npx qodewk --platform <platform>` | Filter telemetry to a specific platform (`antigravity`, `claude`, `cursor`, `all`) | Single-platform audit |
115
115
  | `npx qodewk --json` | Export machine-readable telemetry conforming to canonical `ReceiptV1` schema | Formatted JSON output |
116
116
  | `npx qodewk share` | Publish privacy-safe metadata to Qodewk Cloud and generate short link | `https://qodewk.dev/r/rec_...` |
117
+ | `npx qodewk share --today` | Share today's harvested footprint window | Public URL + claim token |
117
118
  | `npx qodewk audit --base <branch>` | Calculate aggregate diff and telemetry across an entire PR branch range | Git revision delta receipt |
119
+ | `npx qodewk hook install` / `hooks install` | Install non-blocking `post-commit` + `post-rewrite` hooks | Local auto-record |
120
+ | `npx qodewk record` / `record-event` | Silent SQLite record (hook entrypoint) | exit 0 always |
118
121
  | `npx qodewk -f markdown -o receipt.md` | Export sticky Markdown receipt directly formatted for GitHub PR comments | File `receipt.md` |
119
122
  | `npx qodewk -p <provider> -m <model>` | Override detected provider & frontier model pricing rate card | Custom model cost estimate |
120
123
  | `npx qodewk --anon` | Redact sensitive repository and branch identifiers | Privacy-hardened receipt |
124
+ | `npx qodewk --local` | Force local-only mode (no cloud sockets on share) | Local thermal receipt |
121
125
 
122
126
  ---
123
127
 
package/dist/index.cjs CHANGED
@@ -15332,9 +15332,15 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
15332
15332
  let renames = 0;
15333
15333
  const extCounts = {};
15334
15334
  let totalTrackedFiles = 0;
15335
+ const changedFiles = [];
15335
15336
  for (const f of diffSummary.files) {
15336
15337
  if (f.file.includes(" => ")) {
15337
15338
  renames++;
15339
+ const dest = f.file.split(" => ").pop()?.trim();
15340
+ if (dest)
15341
+ changedFiles.push(dest.replace(/\\/g, "/"));
15342
+ } else {
15343
+ changedFiles.push(f.file.replace(/\\/g, "/"));
15338
15344
  }
15339
15345
  const ext = path.extname(f.file).replace(/^\./, "").toLowerCase();
15340
15346
  const lang = mapExtensionToLanguage(ext);
@@ -15358,6 +15364,7 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
15358
15364
  netLines,
15359
15365
  renames,
15360
15366
  languages,
15367
+ changedFiles,
15361
15368
  branch,
15362
15369
  headSha,
15363
15370
  baseSha,
@@ -15367,6 +15374,21 @@ async function extractGitMetrics(optionsOrPath = process.cwd()) {
15367
15374
  commitsCount
15368
15375
  };
15369
15376
  }
15377
+ function computeAiWrittenRatio(changedFiles, aiEditedFiles) {
15378
+ if (changedFiles.length === 0 || aiEditedFiles.length === 0) {
15379
+ return void 0;
15380
+ }
15381
+ const norm = (f) => f.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
15382
+ const aiNorm = aiEditedFiles.map(norm);
15383
+ let hit = 0;
15384
+ for (const cf of changedFiles) {
15385
+ const c3 = norm(cf);
15386
+ const matched = aiNorm.some((a) => a === c3 || c3.endsWith("/" + a) || a.endsWith("/" + c3) || c3.endsWith(a) || a.endsWith(c3));
15387
+ if (matched)
15388
+ hit++;
15389
+ }
15390
+ return Math.round(hit / changedFiles.length * 100) / 100;
15391
+ }
15370
15392
  function mapExtensionToLanguage(ext) {
15371
15393
  const map = {
15372
15394
  ts: "TypeScript",
@@ -16296,9 +16318,12 @@ function formatMarkdownReceipt(receipt, publicUrl) {
16296
16318
  const costPrefix = receipt.ai.mode === "verified" ? "$" : "~$";
16297
16319
  const confidencePercent = `${Math.round(receipt.ai.confidence * 100)}%`;
16298
16320
  const confidenceTier = receipt.ai.confidence >= 0.8 ? "High" : receipt.ai.confidence >= 0.4 ? "Medium" : "Low";
16321
+ const tokenProvenance = receipt.ai.mode === "verified" || receipt.ai.mode === "observed" ? "Observed" : "Estimated";
16322
+ const costProvenance = receipt.ai.mode === "verified" ? "Verified" : receipt.ai.mode === "observed" ? "Observed" : "Estimated";
16323
+ const tokenDisplay = receipt.ai.mode === "verified" || receipt.ai.mode === "observed" ? totalTokens : `~${totalTokens}`;
16299
16324
  const lines = [
16300
16325
  `<!-- QODEWK_RECEIPT_START:${receipt.receipt.id} -->`,
16301
- `### \u{1F9FE} Qodewk Telemetry Receipt`,
16326
+ `### Qodewk Telemetry Receipt`,
16302
16327
  ``,
16303
16328
  `| Metric | Measurement | Provenance |`,
16304
16329
  `| :--- | :--- | :--- |`,
@@ -16307,19 +16332,42 @@ function formatMarkdownReceipt(receipt, publicUrl) {
16307
16332
  `| **Lines Deleted** | \`-${receipt.mutation.deletions}\` | Observed |`,
16308
16333
  `| **Net Delta** | \`${receipt.mutation.netLines >= 0 ? "+" : ""}${receipt.mutation.netLines}\` | Observed |`,
16309
16334
  `| **AI Model** | \`${receipt.ai.provider} \xB7 ${receipt.ai.model || "Unknown"}\` | ${receipt.ai.mode} |`,
16310
- `| **Tokens Consumed** | \`~${totalTokens}\` | Estimated |`,
16311
- `| **AI Spend** | \`${costPrefix}${receipt.ai.cost.toFixed(2)}\` | Estimated |`,
16335
+ `| **Tokens Consumed** | \`${tokenDisplay}\` | ${tokenProvenance} |`,
16336
+ `| **AI Spend** | \`${costPrefix}${receipt.ai.cost.toFixed(2)}\` | ${costProvenance} |`,
16312
16337
  `| **Confidence** | \`${confidencePercent}\` (${confidenceTier}) | Dual-Engine |`,
16313
16338
  ``,
16314
- `> \u{1F512} **Privacy Guarantee:** *Source code was never uploaded to Qodewk. Telemetry computed strictly from cryptographic Git metadata.*`,
16339
+ `> **Privacy Guarantee:** *Source code was never uploaded to Qodewk. Telemetry computed strictly from cryptographic Git metadata.*`,
16315
16340
  ``,
16316
- `\u{1F517} [**View Full Digital Receipt \u2192**](${url})`,
16341
+ `[**View Full Digital Receipt \u2192**](${url})`,
16317
16342
  `<!-- QODEWK_RECEIPT_END -->`
16318
16343
  ];
16319
16344
  return lines.join("\n");
16320
16345
  }
16321
16346
 
16322
16347
  // ../core/dist/index.js
16348
+ function sanitizeReceiptForShare(receipt) {
16349
+ const sessions = receipt.ai.sessions?.map((s) => ({
16350
+ provider: s.provider,
16351
+ model: s.model,
16352
+ task: s.task,
16353
+ tokens: s.tokens,
16354
+ cost: s.cost,
16355
+ confidence: s.confidence,
16356
+ mode: s.mode
16357
+ }));
16358
+ return {
16359
+ ...receipt,
16360
+ privacy: {
16361
+ ...receipt.privacy,
16362
+ sourceExcluded: true,
16363
+ isPublic: true
16364
+ },
16365
+ ai: {
16366
+ ...receipt.ai,
16367
+ sessions
16368
+ }
16369
+ };
16370
+ }
16323
16371
  async function generateReceipt(options = {}) {
16324
16372
  const metrics = await extractGitMetrics({
16325
16373
  repoPath: options.repoPath || process.cwd(),
@@ -16335,7 +16383,7 @@ async function generateReceipt(options = {}) {
16335
16383
  platform: options.platform
16336
16384
  });
16337
16385
  const primaryFp = harvestResult.primaryFootprint;
16338
- let detected = detectProviderFromCommit(metrics.commitMessage);
16386
+ const detected = detectProviderFromCommit(metrics.commitMessage);
16339
16387
  const provider = options.provider || primaryFp?.platform || detected?.provider || "unknown";
16340
16388
  const model = options.model || primaryFp?.model || detected?.model;
16341
16389
  const task = options.task || primaryFp?.taskTitle || (harvestResult.tasks.length > 0 ? harvestResult.tasks[0] : void 0);
@@ -16349,10 +16397,7 @@ async function generateReceipt(options = {}) {
16349
16397
  confidence: fp.confidence,
16350
16398
  mode: fp.mode
16351
16399
  }));
16352
- let aiWrittenRatio = void 0;
16353
- if (primaryFp) {
16354
- aiWrittenRatio = 0.88;
16355
- }
16400
+ const aiWrittenRatio = computeAiWrittenRatio(metrics.changedFiles, harvestResult.filesEdited);
16356
16401
  const costResult = estimateCost({
16357
16402
  files: metrics.files,
16358
16403
  insertions: metrics.insertions,
@@ -16362,18 +16407,22 @@ async function generateReceipt(options = {}) {
16362
16407
  sessions: sessions.length > 0 ? sessions : void 0,
16363
16408
  confidence: primaryFp?.confidence || detected?.confidence
16364
16409
  });
16365
- const receiptId = `rec_${crypto2.randomBytes(12).toString("hex")}`;
16366
16410
  const now = (/* @__PURE__ */ new Date()).toISOString();
16367
16411
  const payloadToHash = {
16368
16412
  repoHash: metrics.repoHash,
16369
16413
  headSha: metrics.headSha,
16414
+ baseSha: metrics.baseSha,
16370
16415
  files: metrics.files,
16371
16416
  insertions: metrics.insertions,
16372
16417
  deletions: metrics.deletions,
16373
16418
  cost: costResult.cost,
16374
- tokens: costResult.tokens
16419
+ tokens: costResult.tokens,
16420
+ mode: costResult.mode,
16421
+ provider: costResult.provider,
16422
+ model: costResult.model
16375
16423
  };
16376
16424
  const contentHash = crypto2.createHash("sha256").update(JSON.stringify(payloadToHash)).digest("hex");
16425
+ const receiptId = `rec_${contentHash.slice(0, 24)}`;
16377
16426
  const receipt = {
16378
16427
  version: "1.0",
16379
16428
  receipt: {
@@ -16410,7 +16459,7 @@ async function generateReceipt(options = {}) {
16410
16459
  },
16411
16460
  privacy: {
16412
16461
  sourceExcluded: true,
16413
- isPublic: options.isPublic ?? true,
16462
+ isPublic: options.isPublic ?? false,
16414
16463
  anonymizeBranch: options.anonymizeBranch ?? false
16415
16464
  }
16416
16465
  };
@@ -16418,154 +16467,279 @@ async function generateReceipt(options = {}) {
16418
16467
  }
16419
16468
 
16420
16469
  // src/index.ts
16470
+ var SHARE_PAYLOAD_MAX_BYTES = 5e4;
16471
+ function resolveCliVersion() {
16472
+ try {
16473
+ const argv1 = process.argv[1] ? path7.dirname(path7.resolve(process.argv[1])) : process.cwd();
16474
+ const candidates = [
16475
+ path7.join(argv1, "..", "package.json"),
16476
+ path7.join(argv1, "package.json")
16477
+ ];
16478
+ for (const candidate of candidates) {
16479
+ if (!fs6.existsSync(candidate)) continue;
16480
+ const pkg = JSON.parse(fs6.readFileSync(candidate, "utf-8"));
16481
+ if (pkg.name === "qodewk" && pkg.version) return pkg.version;
16482
+ }
16483
+ } catch {
16484
+ }
16485
+ return "0.2.1";
16486
+ }
16487
+ function resolveSince(options) {
16488
+ return options.today ? "today" : options.since;
16489
+ }
16490
+ function persistReceipt(receipt, claimToken) {
16491
+ try {
16492
+ const db = new LocalStateDB();
16493
+ db.saveReceipt(receipt, claimToken);
16494
+ db.close();
16495
+ } catch {
16496
+ }
16497
+ }
16498
+ function resolveGitHooksDir(cwd = process.cwd()) {
16499
+ const gitPath = path7.join(cwd, ".git");
16500
+ if (!fs6.existsSync(gitPath)) return null;
16501
+ let gitCommonDir;
16502
+ const stat = fs6.statSync(gitPath);
16503
+ if (stat.isDirectory()) {
16504
+ gitCommonDir = gitPath;
16505
+ } else {
16506
+ const content = fs6.readFileSync(gitPath, "utf-8");
16507
+ const match = content.match(/gitdir:\s*(.+)/i);
16508
+ if (!match?.[1]) return null;
16509
+ let gitDir = match[1].trim();
16510
+ if (!path7.isAbsolute(gitDir)) {
16511
+ gitDir = path7.resolve(cwd, gitDir);
16512
+ }
16513
+ const commonFile = path7.join(gitDir, "commondir");
16514
+ if (fs6.existsSync(commonFile)) {
16515
+ let common = fs6.readFileSync(commonFile, "utf-8").trim();
16516
+ if (!path7.isAbsolute(common)) {
16517
+ common = path7.resolve(gitDir, common);
16518
+ }
16519
+ gitCommonDir = common;
16520
+ } else if (path7.basename(path7.dirname(gitDir)) === "worktrees") {
16521
+ gitCommonDir = path7.dirname(path7.dirname(gitDir));
16522
+ } else {
16523
+ gitCommonDir = gitDir;
16524
+ }
16525
+ }
16526
+ return path7.join(gitCommonDir, "hooks");
16527
+ }
16528
+ var HOOK_MARKER_BEGIN = "# --- BEGIN QODEWK HOOK ---";
16529
+ var HOOK_MARKER_END = "# --- END QODEWK HOOK ---";
16530
+ var HOOK_SNIPPET = `
16531
+ ${HOOK_MARKER_BEGIN}
16532
+ # Non-blocking Qodewk background recorder (< 5ms)
16533
+ if command -v qodewk >/dev/null 2>&1 || command -v pnpm >/dev/null 2>&1 || [ -f "./node_modules/.bin/qodewk" ]; then
16534
+ ( ( qodewk record || pnpm qodewk record || npx qodewk record ) >/dev/null 2>&1 & )
16535
+ fi
16536
+ ${HOOK_MARKER_END}
16537
+ `;
16538
+ function installHookFile(hookFile) {
16539
+ let content = "";
16540
+ if (fs6.existsSync(hookFile)) {
16541
+ content = fs6.readFileSync(hookFile, "utf-8");
16542
+ } else {
16543
+ content = "#!/bin/sh\n";
16544
+ }
16545
+ if (content.includes(HOOK_MARKER_BEGIN)) {
16546
+ return "exists";
16547
+ }
16548
+ fs6.writeFileSync(hookFile, content + HOOK_SNIPPET, { mode: 493 });
16549
+ return "installed";
16550
+ }
16551
+ function uninstallHookFile(hookFile) {
16552
+ if (!fs6.existsSync(hookFile)) return "absent";
16553
+ const content = fs6.readFileSync(hookFile, "utf-8");
16554
+ if (!content.includes(HOOK_MARKER_BEGIN)) return "missing";
16555
+ const regex = new RegExp(`\\n?${HOOK_MARKER_BEGIN}[\\s\\S]*?${HOOK_MARKER_END}\\n?`, "g");
16556
+ const updated = content.replace(regex, "");
16557
+ fs6.writeFileSync(hookFile, updated, { mode: 493 });
16558
+ return "removed";
16559
+ }
16421
16560
  var program2 = new Command();
16422
- program2.name("qodewk").description("Universal telemetry and digital receipt generator for the AI coding agent era").version("0.2.1").option("-j, --json", "Output receipt in machine-readable JSON format").option("-f, --format <format>", "Output format (terminal, json, markdown)", "terminal").option("-o, --out <path>", "Write receipt output to specified file path").option("-p, --provider <provider>", "Specify AI provider (antigravity, claude, cursor, etc.)").option("-m, --model <model>", "Specify AI model identifier").option("-s, --since <duration>", "Harvest agent footprints since duration (e.g. today, 24h, 7d)").option("--today", "Harvest agent footprints for today").option("--platform <platform>", "Filter agent platform (antigravity, claude, cursor, all)").option("--anon", "Anonymize branch name in output").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16561
+ program2.name("qodewk").description("Universal telemetry and digital receipt generator for the AI coding agent era").version(resolveCliVersion()).option("-j, --json", "Output receipt in machine-readable JSON format").option("-f, --format <format>", "Output format (terminal, json, markdown)", "terminal").option("-o, --out <path>", "Write receipt output to specified file path").option("-p, --provider <provider>", "Specify AI provider (antigravity, claude, cursor, etc.)").option("-m, --model <model>", "Specify AI model identifier").option("-s, --since <duration>", "Harvest agent footprints since duration (e.g. today, 24h, 7d)").option("--today", "Harvest agent footprints for today").option("--platform <platform>", "Filter agent platform (antigravity, claude, cursor, all)").option("--anon", "Anonymize branch name in output").option("--local", "Force local-only mode (never open network sockets)").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16423
16562
  try {
16424
- const since = options.today ? "today" : options.since;
16425
16563
  const receipt = await generateReceipt({
16426
16564
  provider: options.provider,
16427
16565
  model: options.model,
16428
- since,
16566
+ since: resolveSince(options),
16429
16567
  platform: options.platform,
16430
- anonymizeBranch: options.anon
16568
+ anonymizeBranch: options.anon,
16569
+ isPublic: false
16431
16570
  });
16432
- try {
16433
- const db = new LocalStateDB();
16434
- db.saveReceipt(receipt);
16435
- db.close();
16436
- } catch {
16437
- }
16571
+ persistReceipt(receipt);
16438
16572
  await outputReceipt(receipt, options);
16439
16573
  } catch (err) {
16440
- console.error(import_picocolors.default.red(`Error generating receipt: ${err.message}`));
16574
+ const message = err instanceof Error ? err.message : String(err);
16575
+ console.error(import_picocolors.default.red(`Error generating receipt: ${message}`));
16441
16576
  process.exit(1);
16442
16577
  }
16443
16578
  });
16444
- program2.command("audit").description("Audit PR or branch diff against a target base commit").option("-b, --base <base>", "Base git ref or commit SHA (e.g. origin/main)").option("-h, --head <head>", "Head git ref or commit SHA").option("-f, --format <format>", "Output format (terminal, json, markdown)", "terminal").option("-o, --out <path>", "Write receipt output to specified file path").option("-p, --provider <provider>", "Specify AI provider").option("-m, --model <model>", "Specify AI model").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16579
+ program2.command("audit").description("Audit PR or branch diff against a target base commit").option("-b, --base <base>", "Base git ref or commit SHA (e.g. origin/main)").option("-h, --head <head>", "Head git ref or commit SHA").option("-f, --format <format>", "Output format (terminal, json, markdown)", "terminal").option("-o, --out <path>", "Write receipt output to specified file path").option("-p, --provider <provider>", "Specify AI provider").option("-m, --model <model>", "Specify AI model").option("-s, --since <duration>", "Harvest agent footprints since duration").option("--today", "Harvest agent footprints for today").option("--platform <platform>", "Filter agent platform").option("--anon", "Anonymize branch name in output").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16445
16580
  try {
16446
16581
  const receipt = await generateReceipt({
16447
16582
  baseSha: options.base,
16448
16583
  headSha: options.head,
16449
16584
  provider: options.provider,
16450
- model: options.model
16585
+ model: options.model,
16586
+ since: resolveSince(options),
16587
+ platform: options.platform,
16588
+ anonymizeBranch: options.anon,
16589
+ isPublic: false
16451
16590
  });
16591
+ persistReceipt(receipt);
16452
16592
  await outputReceipt(receipt, options);
16453
16593
  } catch (err) {
16454
- console.error(import_picocolors.default.red(`Error auditing git range: ${err.message}`));
16594
+ const message = err instanceof Error ? err.message : String(err);
16595
+ console.error(import_picocolors.default.red(`Error auditing git range: ${message}`));
16455
16596
  process.exit(1);
16456
16597
  }
16457
16598
  });
16458
- program2.command("share").description("Publish privacy-safe receipt to Qodewk cloud and get a shareable URL").option("-p, --provider <provider>", "Specify AI provider").option("-m, --model <model>", "Specify AI model").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16599
+ program2.command("share").description("Publish privacy-safe receipt to Qodewk cloud and get a shareable URL").option("-p, --provider <provider>", "Specify AI provider").option("-m, --model <model>", "Specify AI model").option("-s, --since <duration>", "Harvest agent footprints since duration").option("--today", "Harvest agent footprints for today").option("--platform <platform>", "Filter agent platform").option("--anon", "Anonymize branch name in output").option("-f, --format <format>", "Local fallback output format", "terminal").option("-j, --json", "Also print JSON after share").option("--barcode", "Render 1D barcode simulation instead of scannable 2D QR code").action(async (options) => {
16459
16600
  try {
16460
16601
  const receipt = await generateReceipt({
16461
16602
  provider: options.provider,
16462
- model: options.model
16603
+ model: options.model,
16604
+ since: resolveSince(options),
16605
+ platform: options.platform,
16606
+ anonymizeBranch: options.anon,
16607
+ isPublic: false
16463
16608
  });
16464
- if (process.env.QODEWK_TELEMETRY === "off") {
16465
- console.log(import_picocolors.default.yellow("\n\u26A0\uFE0F Cloud publishing is disabled because QODEWK_TELEMETRY=off."));
16609
+ const localFlag = program2.opts().local === true;
16610
+ if (process.env.QODEWK_TELEMETRY === "off" || localFlag) {
16611
+ console.log(
16612
+ import_picocolors.default.yellow(
16613
+ `
16614
+ Cloud publishing is disabled (${localFlag ? "--local" : "QODEWK_TELEMETRY=off"}).`
16615
+ )
16616
+ );
16466
16617
  console.log(import_picocolors.default.dim("Telemetry remains strictly stored in local SQLite (~/.qodewk/state.db).\n"));
16618
+ persistReceipt(receipt);
16467
16619
  await renderTerminalReceipt(receipt, void 0, options.barcode);
16468
16620
  return;
16469
16621
  }
16622
+ const sanitized = sanitizeReceiptForShare(receipt);
16623
+ let body = JSON.stringify(sanitized);
16624
+ if (Buffer.byteLength(body, "utf-8") > SHARE_PAYLOAD_MAX_BYTES && sanitized.ai.sessions) {
16625
+ const trimmed2 = {
16626
+ ...sanitized,
16627
+ ai: { ...sanitized.ai, sessions: void 0 }
16628
+ };
16629
+ body = JSON.stringify(trimmed2);
16630
+ }
16631
+ if (Buffer.byteLength(body, "utf-8") > SHARE_PAYLOAD_MAX_BYTES) {
16632
+ console.error(import_picocolors.default.red("Sanitized receipt still exceeds the 50 KB cloud payload limit."));
16633
+ process.exit(1);
16634
+ }
16470
16635
  const endpoint = process.env.QODEWK_API_URL || `${process.env.NEXT_PUBLIC_APP_URL || process.env.QODEWK_APP_URL || "https://qodewk.flinkeo.online"}/api/receipts`;
16471
- console.log(import_picocolors.default.dim(`Publishing receipt ${receipt.receipt.id} to ${endpoint}...`));
16636
+ console.log(import_picocolors.default.dim(`Publishing receipt ${sanitized.receipt.id} to ${endpoint}...`));
16472
16637
  try {
16473
16638
  const response = await fetch(endpoint, {
16474
16639
  method: "POST",
16475
16640
  headers: {
16476
- "Content-Type": "application/json"
16641
+ "Content-Type": "application/json",
16642
+ "Content-Length": String(Buffer.byteLength(body, "utf-8"))
16477
16643
  },
16478
- body: JSON.stringify(receipt)
16644
+ body
16479
16645
  });
16480
16646
  if (!response.ok) {
16481
- throw new Error(`API responded with status ${response.status}`);
16647
+ const errText = await response.text().catch(() => "");
16648
+ throw new Error(`API responded with status ${response.status}${errText ? `: ${errText.slice(0, 200)}` : ""}`);
16482
16649
  }
16483
16650
  const data = await response.json();
16484
- const db = new LocalStateDB();
16485
- db.saveReceipt(receipt, data.claimToken);
16486
- db.close();
16487
- await renderTerminalReceipt(receipt, data.url, options.barcode);
16651
+ persistReceipt(sanitized, data.claimToken);
16652
+ console.log("");
16653
+ console.log(import_picocolors.default.green(" Published successfully."));
16654
+ console.log(` ${import_picocolors.default.dim("Public URL:")} ${import_picocolors.default.underline(import_picocolors.default.cyan(data.url))}`);
16655
+ console.log(` ${import_picocolors.default.bold(import_picocolors.default.yellow(" Claim token (store securely \u2014 shown once):"))}`);
16656
+ console.log(` ${import_picocolors.default.yellow(data.claimToken)}`);
16657
+ console.log(import_picocolors.default.dim(" Saved to ~/.qodewk/state.db \u2014 required to prove authorship later."));
16658
+ console.log("");
16659
+ await renderTerminalReceipt(sanitized, data.url, options.barcode);
16660
+ if (options.json) {
16661
+ console.log(JSON.stringify({ url: data.url, claimToken: data.claimToken, receipt: sanitized }, null, 2));
16662
+ }
16488
16663
  } catch (networkErr) {
16664
+ const message = networkErr instanceof Error ? networkErr.message : String(networkErr);
16489
16665
  console.log(import_picocolors.default.yellow(`
16490
- Could not reach cloud API (${networkErr.message}). Rendered locally:`));
16666
+ Could not reach cloud API (${message}). Rendered locally:`));
16667
+ persistReceipt(receipt);
16491
16668
  await renderTerminalReceipt(receipt, void 0, options.barcode);
16492
16669
  }
16493
16670
  } catch (err) {
16494
- console.error(import_picocolors.default.red(`Error sharing receipt: ${err.message}`));
16671
+ const message = err instanceof Error ? err.message : String(err);
16672
+ console.error(import_picocolors.default.red(`Error sharing receipt: ${message}`));
16495
16673
  process.exit(1);
16496
16674
  }
16497
16675
  });
16498
- program2.command("record").description("Silently record local telemetry receipt into SQLite (used by git hooks)").action(async () => {
16676
+ program2.command("record").alias("record-event").description("Silently record local telemetry receipt into SQLite (used by git hooks)").action(async () => {
16499
16677
  try {
16500
- const receipt = await generateReceipt();
16501
- const db = new LocalStateDB();
16502
- db.saveReceipt(receipt);
16503
- db.close();
16678
+ const receipt = await generateReceipt({ isPublic: false });
16679
+ persistReceipt(receipt);
16504
16680
  process.exit(0);
16505
16681
  } catch {
16506
16682
  process.exit(0);
16507
16683
  }
16508
16684
  });
16509
- var hookCommand = program2.command("hook").description("Manage non-blocking Git hooks for automatic telemetry recording");
16510
- hookCommand.command("install").description("Install non-blocking post-commit Git hook into current repository").action(async () => {
16685
+ var hookCommand = program2.command("hook").aliases(["hooks"]).description("Manage non-blocking Git hooks for automatic telemetry recording");
16686
+ hookCommand.command("install").description("Install non-blocking post-commit and post-rewrite Git hooks").action(async () => {
16511
16687
  try {
16512
- const gitDir = path7.join(process.cwd(), ".git");
16513
- if (!fs6.existsSync(gitDir)) {
16514
- console.error(import_picocolors.default.red("Error: Current directory is not a Git repository root (.git not found)."));
16688
+ const hooksDir = resolveGitHooksDir();
16689
+ if (!hooksDir) {
16690
+ console.error(import_picocolors.default.red("Error: Current directory is not a Git repository (.git not found)."));
16515
16691
  process.exit(1);
16516
16692
  }
16517
- const hooksDir = path7.join(gitDir, "hooks");
16518
16693
  if (!fs6.existsSync(hooksDir)) {
16519
16694
  fs6.mkdirSync(hooksDir, { recursive: true });
16520
16695
  }
16521
- const hookFile = path7.join(hooksDir, "post-commit");
16522
- const hookMarkerBegin = "# --- BEGIN QODEWK HOOK ---";
16523
- const hookMarkerEnd = "# --- END QODEWK HOOK ---";
16524
- const hookSnippet = `
16525
- ${hookMarkerBegin}
16526
- # Non-blocking Qodewk background recorder (< 5ms)
16527
- if command -v qodewk >/dev/null 2>&1 || command -v pnpm >/dev/null 2>&1 || [ -f "./node_modules/.bin/qodewk" ]; then
16528
- ( ( qodewk record || pnpm qodewk record || npx qodewk record ) >/dev/null 2>&1 & )
16529
- fi
16530
- ${hookMarkerEnd}
16531
- `;
16532
- let content = "";
16533
- if (fs6.existsSync(hookFile)) {
16534
- content = fs6.readFileSync(hookFile, "utf-8");
16535
- } else {
16536
- content = "#!/bin/sh\n";
16696
+ const targets = ["post-commit", "post-rewrite"];
16697
+ let installed = 0;
16698
+ let existing = 0;
16699
+ for (const name of targets) {
16700
+ const result = installHookFile(path7.join(hooksDir, name));
16701
+ if (result === "installed") installed++;
16702
+ else existing++;
16537
16703
  }
16538
- if (content.includes(hookMarkerBegin)) {
16539
- console.log(import_picocolors.default.yellow("Qodewk post-commit hook is already installed."));
16704
+ if (installed === 0 && existing > 0) {
16705
+ console.log(import_picocolors.default.yellow("Qodewk Git hooks are already installed (post-commit, post-rewrite)."));
16540
16706
  return;
16541
16707
  }
16542
- fs6.writeFileSync(hookFile, content + hookSnippet, { mode: 493 });
16543
- console.log(import_picocolors.default.green("\u2713 Non-blocking Qodewk post-commit hook successfully installed in .git/hooks/post-commit"));
16708
+ console.log(
16709
+ import_picocolors.default.green(
16710
+ `\u2713 Non-blocking Qodewk hooks installed in ${hooksDir} (post-commit${installed > 1 || existing > 0 ? ", post-rewrite" : installed === 1 && targets.length === 2 ? " + post-rewrite" : ""})`
16711
+ )
16712
+ );
16713
+ if (existing > 0) {
16714
+ console.log(import_picocolors.default.dim(` (${existing} hook file(s) already contained the Qodewk marker)`));
16715
+ }
16544
16716
  } catch (err) {
16545
- console.error(import_picocolors.default.red(`Failed to install Git hook: ${err.message}`));
16717
+ const message = err instanceof Error ? err.message : String(err);
16718
+ console.error(import_picocolors.default.red(`Failed to install Git hook: ${message}`));
16546
16719
  process.exit(1);
16547
16720
  }
16548
16721
  });
16549
- hookCommand.command("uninstall").description("Remove Qodewk post-commit Git hook").action(async () => {
16722
+ hookCommand.command("uninstall").description("Remove Qodewk post-commit and post-rewrite Git hooks").action(async () => {
16550
16723
  try {
16551
- const hookFile = path7.join(process.cwd(), ".git", "hooks", "post-commit");
16552
- if (!fs6.existsSync(hookFile)) {
16553
- console.log(import_picocolors.default.yellow("No post-commit hook found."));
16724
+ const hooksDir = resolveGitHooksDir();
16725
+ if (!hooksDir) {
16726
+ console.log(import_picocolors.default.yellow("No Git repository found."));
16554
16727
  return;
16555
16728
  }
16556
- const content = fs6.readFileSync(hookFile, "utf-8");
16557
- const hookMarkerBegin = "# --- BEGIN QODEWK HOOK ---";
16558
- const hookMarkerEnd = "# --- END QODEWK HOOK ---";
16559
- if (!content.includes(hookMarkerBegin)) {
16560
- console.log(import_picocolors.default.yellow("Qodewk hook is not installed."));
16729
+ const targets = ["post-commit", "post-rewrite"];
16730
+ let removed = 0;
16731
+ for (const name of targets) {
16732
+ const result = uninstallHookFile(path7.join(hooksDir, name));
16733
+ if (result === "removed") removed++;
16734
+ }
16735
+ if (removed === 0) {
16736
+ console.log(import_picocolors.default.yellow("Qodewk hooks are not installed."));
16561
16737
  return;
16562
16738
  }
16563
- const regex = new RegExp(`\\n?${hookMarkerBegin}[\\s\\S]*?${hookMarkerEnd}\\n?`, "g");
16564
- const updated = content.replace(regex, "");
16565
- fs6.writeFileSync(hookFile, updated, { mode: 493 });
16566
- console.log(import_picocolors.default.green("\u2713 Qodewk post-commit hook removed."));
16739
+ console.log(import_picocolors.default.green(`\u2713 Qodewk hook markers removed from ${removed} file(s).`));
16567
16740
  } catch (err) {
16568
- console.error(import_picocolors.default.red(`Failed to uninstall hook: ${err.message}`));
16741
+ const message = err instanceof Error ? err.message : String(err);
16742
+ console.error(import_picocolors.default.red(`Failed to uninstall hook: ${message}`));
16569
16743
  process.exit(1);
16570
16744
  }
16571
16745
  });
@@ -16576,64 +16750,48 @@ var ansiHex = (hexColor) => {
16576
16750
  const b2 = num & 255;
16577
16751
  return (text) => `\x1B[38;2;${r2};${g};${b2}m${text}\x1B[39m`;
16578
16752
  };
16579
- async function outputReceipt(receipt, options, publicUrl) {
16580
- let content = "";
16581
- const format = options.json ? "json" : options.format || "terminal";
16582
- if (format === "json") {
16583
- content = JSON.stringify(receipt, null, 2);
16584
- } else if (format === "markdown") {
16585
- content = formatMarkdownReceipt(receipt, publicUrl);
16586
- }
16587
- if (options.out) {
16588
- fs6.writeFileSync(options.out, content || formatMarkdownReceipt(receipt, publicUrl), "utf-8");
16589
- console.log(import_picocolors.default.green(`\u2713 Receipt written to ${options.out}`));
16590
- }
16591
- if (format === "terminal" && !options.out) {
16592
- await renderTerminalReceipt(receipt, publicUrl, options.barcode);
16593
- } else if (format !== "terminal" && !options.out) {
16594
- console.log(content);
16595
- }
16596
- }
16597
16753
  var stripAnsi = (str) => {
16598
16754
  return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\([B0]/g, "");
16599
16755
  };
16600
16756
  var visibleWidth = (str) => {
16601
16757
  return stripAnsi(str).length;
16602
16758
  };
16603
- async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16759
+ function buildTerminalReceipt(receipt, publicUrl, useBarcode) {
16760
+ const lines = [];
16604
16761
  const coral = ansiHex("#cc785c");
16605
16762
  const green = ansiHex("#5db872");
16606
16763
  const red = ansiHex("#c64545");
16607
16764
  const teal = ansiHex("#5db8a6");
16608
16765
  const INNER_WIDTH = 52;
16766
+ const push = (line) => lines.push(line);
16609
16767
  const printRow = (content, width = INNER_WIDTH) => {
16610
16768
  const visLen = visibleWidth(content);
16611
16769
  const pad = Math.max(0, width - visLen);
16612
- console.log(` | ${content}${" ".repeat(pad)} |`);
16770
+ push(` | ${content}${" ".repeat(pad)} |`);
16613
16771
  };
16614
16772
  const printRowSplit = (left, right, width = INNER_WIDTH) => {
16615
16773
  const leftVis = visibleWidth(left);
16616
16774
  const rightVis = visibleWidth(right);
16617
16775
  const pad = Math.max(1, width - leftVis - rightVis);
16618
- console.log(` | ${left}${" ".repeat(pad)}${right} |`);
16776
+ push(` | ${left}${" ".repeat(pad)}${right} |`);
16619
16777
  };
16620
16778
  const printCenteredRow = (content, width = INNER_WIDTH) => {
16621
16779
  const visLen = visibleWidth(content);
16622
16780
  const totalPad = Math.max(0, width - visLen);
16623
16781
  const leftPad = Math.floor(totalPad / 2);
16624
16782
  const rightPad = totalPad - leftPad;
16625
- console.log(` | ${" ".repeat(leftPad)}${content}${" ".repeat(rightPad)} |`);
16783
+ push(` | ${" ".repeat(leftPad)}${content}${" ".repeat(rightPad)} |`);
16626
16784
  };
16627
16785
  const printDivider = (char = "=", width = INNER_WIDTH) => {
16628
- console.log(` | ${char.repeat(width)} |`);
16786
+ push(` | ${char.repeat(width)} |`);
16629
16787
  };
16630
16788
  const totalTokens = (receipt.ai.tokens.input + receipt.ai.tokens.output).toLocaleString();
16631
16789
  const inputK = `${Math.round(receipt.ai.tokens.input / 1e3)}k`;
16632
16790
  const outputK = `${Math.round(receipt.ai.tokens.output / 1e3)}k`;
16633
16791
  const costPrefix = receipt.ai.mode === "verified" ? "$" : "~$";
16634
16792
  const confidencePercent = `${Math.round(receipt.ai.confidence * 100)}%`;
16635
- console.log("");
16636
- console.log(" " + import_picocolors.default.bold(coral("/\\".repeat(28))));
16793
+ push("");
16794
+ push(" " + import_picocolors.default.bold(coral("/\\".repeat(28))));
16637
16795
  printCenteredRow(import_picocolors.default.bold(import_picocolors.default.white("Q O D E W K")));
16638
16796
  printCenteredRow(coral("*** PROOF OF SHIPMENT ***"));
16639
16797
  printRow("");
@@ -16675,19 +16833,12 @@ async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16675
16833
  printRowSplit("Total Tokens:", totalTokens);
16676
16834
  printDivider("=");
16677
16835
  const costLabel = receipt.ai.mode === "verified" ? "VERIFIED AI COST" : "ESTIMATED AI COST";
16678
- printRowSplit(
16679
- costLabel,
16680
- import_picocolors.default.bold(coral(costPrefix + receipt.ai.cost.toFixed(2)))
16681
- );
16682
- printRowSplit(
16683
- `CONFIDENCE: ${confidencePercent}`,
16684
- `[Mode: ${receipt.ai.mode}]`
16685
- );
16836
+ printRowSplit(costLabel, import_picocolors.default.bold(coral(costPrefix + receipt.ai.cost.toFixed(2))));
16837
+ printRowSplit(`CONFIDENCE: ${confidencePercent}`, `[Mode: ${receipt.ai.mode}]`);
16686
16838
  printDivider("=");
16687
16839
  printRow("");
16688
16840
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.QODEWK_APP_URL || "https://qodewk.flinkeo.online";
16689
16841
  const isPublished = Boolean(publicUrl);
16690
- const urlToDisplay = publicUrl || `${baseUrl}/r/${receipt.receipt.id}`;
16691
16842
  const displayHost = baseUrl.replace(/^https?:\/\//, "");
16692
16843
  if (useBarcode) {
16693
16844
  const barChars = ["||| ", "| | ", "|||| ", "|| | ", "|| || "];
@@ -16698,10 +16849,10 @@ async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16698
16849
  }
16699
16850
  barcodePattern += "|||";
16700
16851
  printCenteredRow(import_picocolors.default.bold(barcodePattern));
16701
- } else {
16702
- const qrRows = renderTerminalQr(urlToDisplay);
16852
+ } else if (isPublished && publicUrl) {
16853
+ const qrRows = renderTerminalQr(publicUrl);
16703
16854
  if (qrRows.length > 0) {
16704
- printCenteredRow(import_picocolors.default.dim(isPublished ? "--- SCAN PUBLIC RECEIPT ---" : "--- LOCAL PROOF OF WORK ---"));
16855
+ printCenteredRow(import_picocolors.default.dim("--- SCAN PUBLIC RECEIPT ---"));
16705
16856
  printRow("");
16706
16857
  for (const qrRow of qrRows) {
16707
16858
  printCenteredRow(qrRow);
@@ -16710,6 +16861,9 @@ async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16710
16861
  } else {
16711
16862
  printCenteredRow(import_picocolors.default.bold("||| | ||||| ||| |||| |||||| |||| ||| ||||||| |||"));
16712
16863
  }
16864
+ } else {
16865
+ printCenteredRow(import_picocolors.default.dim("--- LOCAL PROOF (NOT PUBLISHED) ---"));
16866
+ printCenteredRow(import_picocolors.default.dim(`id ${receipt.receipt.id.slice(0, 28)}`));
16713
16867
  }
16714
16868
  if (isPublished) {
16715
16869
  printCenteredRow(import_picocolors.default.cyan(displayHost));
@@ -16722,17 +16876,45 @@ async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16722
16876
  printRow(` ${teal("[\u2713]")} QODEWK_TELEMETRY=off (Cloud sync disabled)`);
16723
16877
  }
16724
16878
  printRow(` ${green("[\u2713]")} Source code was never uploaded to Qodewk`);
16725
- console.log(" " + import_picocolors.default.bold(coral("\\/".repeat(28))));
16726
- console.log("");
16727
- if (isPublished) {
16728
- console.log(` ${import_picocolors.default.dim("\u{1F517} Public Receipt:")} ${import_picocolors.default.underline(import_picocolors.default.cyan(urlToDisplay))}`);
16879
+ push(" " + import_picocolors.default.bold(coral("\\/".repeat(28))));
16880
+ push("");
16881
+ if (isPublished && publicUrl) {
16882
+ push(` ${import_picocolors.default.dim("Public Receipt:")} ${import_picocolors.default.underline(import_picocolors.default.cyan(publicUrl))}`);
16729
16883
  } else {
16730
- console.log(` ${import_picocolors.default.dim("\u{1F4BE} Saved to local DB:")} ${import_picocolors.default.dim("~/.qodewk/state.db")}`);
16731
- console.log(` ${import_picocolors.default.dim("\u{1F4A1} To publish & get shareable URL:")} ${import_picocolors.default.cyan("qodewk share")}`);
16884
+ push(` ${import_picocolors.default.dim("Saved to local DB:")} ${import_picocolors.default.dim("~/.qodewk/state.db")}`);
16885
+ push(` ${import_picocolors.default.dim("To publish & get shareable URL:")} ${import_picocolors.default.cyan("qodewk share")}`);
16732
16886
  }
16733
16887
  if (!useBarcode) {
16734
- console.log(` ${import_picocolors.default.dim("\u{1F4CA} View classic 1D barcode:")} ${import_picocolors.default.cyan("qodewk --barcode")}`);
16888
+ push(` ${import_picocolors.default.dim("View classic 1D barcode:")} ${import_picocolors.default.cyan("qodewk --barcode")}`);
16889
+ }
16890
+ push("");
16891
+ return lines.join("\n");
16892
+ }
16893
+ async function renderTerminalReceipt(receipt, publicUrl, useBarcode) {
16894
+ console.log(buildTerminalReceipt(receipt, publicUrl, useBarcode));
16895
+ }
16896
+ async function outputReceipt(receipt, options, publicUrl) {
16897
+ const format = options.json ? "json" : options.format || "terminal";
16898
+ let content;
16899
+ if (format === "json") {
16900
+ content = JSON.stringify(receipt, null, 2);
16901
+ } else if (format === "markdown") {
16902
+ content = formatMarkdownReceipt(receipt, publicUrl);
16903
+ } else {
16904
+ content = buildTerminalReceipt(receipt, publicUrl, options.barcode);
16905
+ }
16906
+ if (options.out) {
16907
+ const fileContent = format === "terminal" ? stripAnsi(content) : content;
16908
+ fs6.writeFileSync(options.out, fileContent.endsWith("\n") ? fileContent : fileContent + "\n", "utf-8");
16909
+ console.log(import_picocolors.default.green(`\u2713 Receipt written to ${options.out}`));
16910
+ }
16911
+ if (!options.out) {
16912
+ if (format === "terminal") {
16913
+ console.log(content);
16914
+ } else {
16915
+ console.log(content);
16916
+ }
16917
+ } else if (format !== "terminal") {
16735
16918
  }
16736
- console.log("");
16737
16919
  }
16738
16920
  program2.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodewk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Universal telemetry and digital receipt generator for the AI coding agent era",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "tsup",
16
16
  "typecheck": "tsc --noEmit",
17
+ "test": "node --test test/smoke.test.mjs",
17
18
  "prepublishOnly": "pnpm run build"
18
19
  },
19
20
  "publishConfig": {
@@ -43,8 +44,7 @@
43
44
  "dependencies": {
44
45
  "commander": "^13.1.0",
45
46
  "picocolors": "^1.1.1",
46
- "qrcode": "^1.5.4",
47
- "simple-git": "^3.27.0"
47
+ "qrcode": "^1.5.4"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@qodewk/core": "workspace:*",