madeby 0.3.10 → 0.3.12

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/README.md CHANGED
@@ -156,7 +156,7 @@ Two modes, nothing more:
156
156
  - optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
157
157
  disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
158
158
  and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
159
- `CURSOR_MODEL`, the Copilot CLI and Claude Code session logs (locally; only the model id is read) —
159
+ `CURSOR_MODEL` or its chat store, the Copilot CLI and Claude Code session logs (locally; only the model id is read) —
160
160
  which wins over an agent's `--model`, since an agent routed by "Auto" may not know which model served
161
161
  it. Elsewhere `--model <version>` is used, never guessed. So `coverage` can list each **agent · model** pairing
162
162
  (`Cursor · gpt-5.3-codex` vs `Claude Code · claude-sonnet-5`). A request, not a gate — an AI
package/dist/madeby.mjs CHANGED
@@ -127,11 +127,12 @@ function classifyCommit(commit) {
127
127
  const botText = `${commit.authorName ?? ""} ${commit.authorEmail ?? ""}`.trim();
128
128
  const agentAi = botText ? matchAi(botText) : null;
129
129
  if (agentAi) {
130
+ const named = [...commit.message.matchAll(AI_AUTHOR_TRAILER), ...commit.message.matchAll(TRAILER_RE)].map((m) => matchAiTrailer(m[1].trim())).filter((c) => c !== null);
130
131
  return {
131
132
  class: "ai",
132
133
  confidence: CONF_AI,
133
- signals: [`author:${agentAi.provider}`],
134
- aiContributors: [{ ...agentAi, raw: botText }]
134
+ signals: [`author:${agentAi.provider}`, ...named.map((c) => `trailer:${c.provider}`)],
135
+ aiContributors: [{ ...agentAi, raw: botText }, ...named]
135
136
  };
136
137
  }
137
138
  return { class: "bot", confidence: CONF_BOT, signals: ["author:bot"], aiContributors };
@@ -1833,12 +1834,13 @@ To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own author
1833
1834
 
1834
1835
  // src/commands/me.ts
1835
1836
  import { execFileSync as execFileSync5 } from "node:child_process";
1836
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
1837
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
1837
1838
  import { homedir as homedir3 } from "node:os";
1838
1839
  import { join as join6 } from "node:path";
1839
1840
 
1840
1841
  // src/model-source.ts
1841
- import { closeSync, existsSync as existsSync4, openSync, readdirSync as readdirSync3, readSync, statSync as statSync2 } from "node:fs";
1842
+ import { createHash } from "node:crypto";
1843
+ import { closeSync, existsSync as existsSync4, openSync, readdirSync as readdirSync3, readFileSync as readFileSync6, readSync, statSync as statSync2 } from "node:fs";
1842
1844
  import { homedir as homedir2 } from "node:os";
1843
1845
  import { join as join5 } from "node:path";
1844
1846
  var SESSION_ID = /^[0-9a-f-]{8,64}$/i;
@@ -1859,11 +1861,32 @@ function lastModelIn(path) {
1859
1861
  if (fd !== void 0) closeSync(fd);
1860
1862
  }
1861
1863
  }
1864
+ var RECENT_MS = 30 * 60 * 1e3;
1865
+ function cursorChatModel(home, cwd) {
1866
+ const dir = join5(home, ".cursor", "chats", createHash("md5").update(cwd).digest("hex"));
1867
+ try {
1868
+ const chats = readdirSync3(dir).map((c) => ({ c, t: statSync2(join5(dir, c)).mtimeMs })).sort((a, b) => b.t - a.t);
1869
+ const newest = chats[0];
1870
+ if (!newest || Date.now() - newest.t > RECENT_MS) return null;
1871
+ const db = join5(dir, newest.c, "store.db");
1872
+ if (!existsSync4(db) || statSync2(db).size > 64 * 1024 * 1024) return null;
1873
+ const names = new Set([...readFileSync6(db).toString("latin1").matchAll(/"modelName"\s*:\s*"([^"]{1,80})"/g)].map((m) => m[1]));
1874
+ return names.size === 1 ? [...names][0] : null;
1875
+ } catch {
1876
+ return null;
1877
+ }
1878
+ }
1862
1879
  var MODEL_SOURCES = [
1863
1880
  {
1864
1881
  tool: "Cursor",
1865
1882
  matches: /\bcursor/i,
1866
- detect: (env) => env.CURSOR_AGENT === "1" && env.CURSOR_MODEL?.trim() ? { model: env.CURSOR_MODEL.trim(), source: "Cursor's CURSOR_MODEL" } : null
1883
+ // Cursor's agent shell sets CURSOR_MODEL — but not always (none of four Auto trials saw it), so its
1884
+ // chat store is the second witness. Either is the tool's record, not the agent's guess.
1885
+ detect: (env, home, cwd) => {
1886
+ if (env.CURSOR_AGENT === "1" && env.CURSOR_MODEL?.trim()) return { model: env.CURSOR_MODEL.trim(), source: "Cursor's CURSOR_MODEL" };
1887
+ const model = cursorChatModel(home, cwd);
1888
+ return model ? { model, source: "Cursor's chat store" } : null;
1889
+ }
1867
1890
  },
1868
1891
  {
1869
1892
  tool: "GitHub Copilot CLI",
@@ -1897,11 +1920,11 @@ var MODEL_SOURCES = [
1897
1920
  }
1898
1921
  }
1899
1922
  ];
1900
- function detectAgentModel(tool, env = process.env, home = homedir2()) {
1923
+ function detectAgentModel(tool, env = process.env, home = homedir2(), cwd = process.cwd()) {
1901
1924
  const generic = /^(?:AI|an? AI)$/i.test(tool.trim());
1902
1925
  for (const s of MODEL_SOURCES) {
1903
1926
  if (!generic && !s.matches.test(tool)) continue;
1904
- const hit = s.detect(env, home);
1927
+ const hit = s.detect(env, home, cwd);
1905
1928
  if (hit) return { tool: generic ? s.tool : tool, ...hit };
1906
1929
  }
1907
1930
  return null;
@@ -1938,14 +1961,14 @@ function recordsModel(root) {
1938
1961
  const p = join6(root, ".madeby", "policy.json");
1939
1962
  if (!existsSync5(p)) return false;
1940
1963
  try {
1941
- return parseDisclosurePolicy(JSON.parse(readFileSync6(p, "utf8"))).recordModel === true;
1964
+ return parseDisclosurePolicy(JSON.parse(readFileSync7(p, "utf8"))).recordModel === true;
1942
1965
  } catch {
1943
1966
  return false;
1944
1967
  }
1945
1968
  }
1946
1969
  function resolveAi(root, o) {
1947
1970
  const tool = o.tool && o.tool.trim() || "AI";
1948
- const detected = recordsModel(root) ? detectAgentModel(tool, o.env ?? process.env, o.home ?? homedir3()) : null;
1971
+ const detected = recordsModel(root) ? detectAgentModel(tool, o.env ?? process.env, o.home ?? homedir3(), o.cwd ?? process.cwd()) : null;
1949
1972
  if (detected) return { value: aiValue(detected.tool, detected.model), tool: detected.tool, model: detected.model, modelSource: detected.source };
1950
1973
  const model = o.model?.trim() || void 0;
1951
1974
  return { value: aiValue(tool, model), tool, ...model ? { model, modelSource: "as given (--model)" } : {} };
@@ -2213,13 +2236,13 @@ function whoCommand(args) {
2213
2236
  }
2214
2237
 
2215
2238
  // src/commands/coverage.ts
2216
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
2239
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
2217
2240
  import { join as join7 } from "node:path";
2218
2241
  function policyIsRequired(root) {
2219
2242
  const p = join7(root, ".madeby", "policy.json");
2220
2243
  if (!existsSync6(p)) return false;
2221
2244
  try {
2222
- return parseDisclosurePolicy(JSON.parse(readFileSync7(p, "utf8"))).mode === "required";
2245
+ return parseDisclosurePolicy(JSON.parse(readFileSync8(p, "utf8"))).mode === "required";
2223
2246
  } catch {
2224
2247
  return false;
2225
2248
  }
@@ -2281,7 +2304,7 @@ function coverageCommand(args) {
2281
2304
  }
2282
2305
 
2283
2306
  // src/init-repo.ts
2284
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
2307
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
2285
2308
  import { dirname as dirname2, join as join8 } from "node:path";
2286
2309
  var ACO_VERSION = 2;
2287
2310
  var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
@@ -2357,7 +2380,7 @@ function ensurePolicy(abs, rel, want, res) {
2357
2380
  }
2358
2381
  let raw;
2359
2382
  try {
2360
- raw = JSON.parse(readFileSync8(abs, "utf8"));
2383
+ raw = JSON.parse(readFileSync9(abs, "utf8"));
2361
2384
  } catch {
2362
2385
  res.skipped.push(rel);
2363
2386
  return { mode: "advisory", recordModel: false, malformed: true };
@@ -2387,7 +2410,7 @@ ${block}`);
2387
2410
  res.created.push(rel);
2388
2411
  return;
2389
2412
  }
2390
- const content = readFileSync8(abs, "utf8");
2413
+ const content = readFileSync9(abs, "utf8");
2391
2414
  const status = acoStatus(content);
2392
2415
  const loc = findAcoBlock(content);
2393
2416
  if (status === "absent") {
@@ -2506,7 +2529,7 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
2506
2529
  import { execFileSync as execFileSync6 } from "node:child_process";
2507
2530
  import { createInterface } from "node:readline";
2508
2531
  var DEFAULT_PROTOCOL = "2024-11-05";
2509
- var SERVER_INFO = { name: "madeby", version: "0.3.10" };
2532
+ var SERVER_INFO = { name: "madeby", version: "0.3.12" };
2510
2533
  function rootFor(path) {
2511
2534
  const cwd = path && path.length ? path : process.cwd();
2512
2535
  return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
@@ -2617,14 +2640,19 @@ var ALIGN_GUIDE = `# Aligning a repository with MadeBy
2617
2640
  MadeBy is **disclosure, not detection** \u2014 it records what a commit *states* about its origin, never
2618
2641
  guesses whether code is AI. To align a repo:
2619
2642
 
2620
- 1. **Assess** \u2014 call \`check\` (or \`who\` with no path for a repo-wide map) to see current coverage.
2621
- 2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory) and the CI disclosure check.
2622
- Idempotent; it never clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an
2623
- advisory policy to required). Then commit the created files.
2624
- 3. **Disclose going forward** \u2014 contributors disclose origin with a Co-Authored-By / Generated-by
2625
- trailer (AI), an \`Authored-by-human\` trailer (work they wrote themselves \u2014 the symmetric human
2626
- claim; \`madeby me\` adds it), a DCO Signed-off-by, a signed commit, or \`ai\` (record your own AI
2627
- session's witnessed spans, structurally verified and local). Human and AI disclosure ride the same ladder.
2643
+ 1. **Assess** \u2014 call \`coverage\` for the scoreboard (known vs unknown, the human / with_ai / ai mix, and
2644
+ the agents and model versions named), \`check\` for the gate's verdict, or \`who\` with no path for a
2645
+ repo-wide map.
2646
+ 2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory), the AGENTS.md disclosure rule,
2647
+ and the CI disclosure check (\`host: "azure-devops"\` for Azure Pipelines). Idempotent; it never
2648
+ clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an advisory policy to
2649
+ required). Add \`record_model: true\` if the repo should also record which model version did the work
2650
+ (opt-in; a project's choice). Then commit the created files.
2651
+ 3. **Disclose going forward** \u2014 contributors disclose origin with an \`Authored-by-ai\` trailer (an AI
2652
+ wrote it; \`madeby ai\` adds it), \`Authored-by-human\` plus \`Assisted-by\` (a human wrote it with AI;
2653
+ \`madeby me --with-ai\`), \`Authored-by-human\` alone (\`madeby me\`), a Co-Authored-By / Generated-by AI
2654
+ trailer, a DCO Signed-off-by, a signed commit, or witnessed spans from an AI session log
2655
+ (\`madeby ai --witness\`). Human and AI disclosure ride the same ladder.
2628
2656
  4. **Account for existing history honestly** \u2014 do NOT fabricate disclosure for past commits. Read the
2629
2657
  \`madeby://guide/backfill\` resource: recognize what's already disclosed, attach recoverable
2630
2658
  evidence, and set an adoption boundary \u2014 pre-boundary history is labeled *unknown*, never invented.
@@ -2704,7 +2732,8 @@ var POLICY_SCHEMA = {
2704
2732
  properties: {
2705
2733
  version: { const: 0 },
2706
2734
  mode: { enum: ["advisory", "required"], description: "advisory: report only (the default, and the safe degrade); required: fail on any undisclosed commit" },
2707
- accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any recognized disclosure counts" }
2735
+ accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any recognized disclosure counts" },
2736
+ recordModel: { type: "boolean", description: "ask AI disclosures to name the model version too (tool:model, e.g. Assisted-by: Cursor:gpt-5.3-codex); madeby reads it from the tool's own session record where it can. Set by init --record-model. A request, not a gate" }
2708
2737
  }
2709
2738
  };
2710
2739
  var ACO_GUIDE = `# ACO \u2014 the Authorship Certificate of Origin
@@ -2720,12 +2749,22 @@ https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md
2720
2749
  - **ai** \u2014 an AI agent authored it (a human remains accountable).
2721
2750
 
2722
2751
  ## How an agent discloses its OWN work (asserted tier, no keys)
2723
- - Collaborative / human-directed work you just did -> call **disclose** (with_ai: true, tool: "<your name>").
2724
- It affirms human authorship + discloses AI assistance on HEAD, classifying the commit with_ai.
2752
+ - After you commit, call **disclose** with the honest \`category\` and your \`tool\` name (e.g. "Claude Code"):
2753
+ - \`ai\`: you wrote it; the committer stays accountable. Adds \`Authored-by-ai: <tool>\`.
2754
+ - \`with_ai\`: a human wrote it with your help. Adds \`Authored-by-human\` + \`Assisted-by: <tool>\`.
2755
+ - \`human\`: a human wrote it alone. Adds \`Authored-by-human\`.
2756
+ If you wrote the code and a human is only committing it, that is \`ai\`, not \`with_ai\`.
2757
+ - **The model version.** If the repo's \`.madeby/policy.json\` has \`"recordModel": true\`, madeby reads your
2758
+ model from the tool's own record of the session (Cursor, Copilot CLI, Claude Code) and writes
2759
+ \`<tool>:<model>\`. Elsewhere pass \`model\` only if you know your exact model version. Never guess.
2760
+ - **If your platform makes or rebuilds the commit** (a cloud coding agent that pushes through its own
2761
+ tool), a trailer amended in afterwards can be lost. Call disclose with \`print: true\` to get the trailer
2762
+ lines instead, and put them in the commit message's last paragraph.
2725
2763
  - Fully AI-authored spans recoverable from your session log -> call **ai** (records structurally-verified
2726
2764
  spans, local-only).
2727
- - Read what's already disclosed -> **who**. Align a whole repo -> **init** (also scaffolds an AGENTS.md
2728
- ACO convention so any agent working there discloses).
2765
+ - Read what's already disclosed -> **who**; measure a repo -> **coverage** (also lists the agents and
2766
+ models named). Align a whole repo -> **init** (also scaffolds an AGENTS.md ACO convention so any agent
2767
+ working there discloses).
2729
2768
 
2730
2769
  Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
2731
2770
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "mcpName": "io.github.MadeByFYI/madeby",
5
5
  "description": "Disclose AI authorship of code. `npx madeby ai` adds an Authored-by-ai git trailer; AI coding agents disclose per the ACO standard (human/with_ai/ai) from an AGENTS.md convention. Disclosure, not detection. Gate PRs with `madeby check`.",
6
6
  "license": "MIT",