madeby 0.3.9 → 0.3.11
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 +4 -1
- package/dist/madeby.mjs +83 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,9 @@ madeby ai [tool] Made by AI: attest AI authorship of HEAD (an Auth
|
|
|
54
54
|
`--witness [log]` instead records witnessed spans from a session log.
|
|
55
55
|
madeby me [--with-ai [t]] Made by me: affirm you authored HEAD (Authored-by-human); --with-ai adds
|
|
56
56
|
Assisted-by → with_ai. --model <version> names the model too.
|
|
57
|
+
--print (me / ai) print the trailer lines instead of amending HEAD — for a
|
|
58
|
+
platform that makes the commit itself (a cloud coding agent):
|
|
59
|
+
git commit -m "<msg>" -m "$(madeby ai <tool> --print)".
|
|
57
60
|
madeby mcp Run the MCP server (stdio) — the tools, agent-callable.
|
|
58
61
|
madeby help
|
|
59
62
|
```
|
|
@@ -153,7 +156,7 @@ Two modes, nothing more:
|
|
|
153
156
|
- optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
|
|
154
157
|
disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
|
|
155
158
|
and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
|
|
156
|
-
`CURSOR_MODEL
|
|
159
|
+
`CURSOR_MODEL` or its chat store, the Copilot CLI and Claude Code session logs (locally; only the model id is read) —
|
|
157
160
|
which wins over an agent's `--model`, since an agent routed by "Auto" may not know which model served
|
|
158
161
|
it. Elsewhere `--model <version>` is used, never guessed. So `coverage` can list each **agent · model** pairing
|
|
159
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
|
|
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 {
|
|
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
|
-
|
|
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",
|
|
@@ -1872,7 +1895,7 @@ var MODEL_SOURCES = [
|
|
|
1872
1895
|
const id = env.COPILOT_AGENT_SESSION_ID;
|
|
1873
1896
|
if (env.COPILOT_CLI !== "1" || !id || !SESSION_ID.test(id)) return null;
|
|
1874
1897
|
const model = lastModelIn(join5(home, ".copilot", "session-state", id, "events.jsonl"));
|
|
1875
|
-
return model ? { model, source: "Copilot
|
|
1898
|
+
return model ? { model, source: "Copilot's session log" } : null;
|
|
1876
1899
|
}
|
|
1877
1900
|
},
|
|
1878
1901
|
{
|
|
@@ -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(
|
|
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)" } : {} };
|
|
@@ -1979,6 +2002,24 @@ function attestAiAuthorship(root, opts = {}) {
|
|
|
1979
2002
|
}
|
|
1980
2003
|
return { ...base, ok: true, changed: trailers.length > 0, subject, added };
|
|
1981
2004
|
}
|
|
2005
|
+
function disclosureTrailers(root, opts) {
|
|
2006
|
+
if (opts.category === "ai") {
|
|
2007
|
+
const ai2 = resolveAi(root, opts);
|
|
2008
|
+
return { ok: true, lines: [`Authored-by-ai: ${ai2.value}`], model: ai2.model, modelSource: ai2.modelSource };
|
|
2009
|
+
}
|
|
2010
|
+
let name = "";
|
|
2011
|
+
let email = "";
|
|
2012
|
+
try {
|
|
2013
|
+
name = git3(root, ["config", "user.name"]);
|
|
2014
|
+
email = git3(root, ["config", "user.email"]);
|
|
2015
|
+
} catch {
|
|
2016
|
+
}
|
|
2017
|
+
if (!name || !email) return { ok: false, reason: "no-identity", lines: [] };
|
|
2018
|
+
const lines = [`Authored-by-human: ${name} <${email}>`];
|
|
2019
|
+
if (opts.category === "human") return { ok: true, lines };
|
|
2020
|
+
const ai = resolveAi(root, opts);
|
|
2021
|
+
return { ok: true, lines: [...lines, `Assisted-by: ${ai.value}`], model: ai.model, modelSource: ai.modelSource };
|
|
2022
|
+
}
|
|
1982
2023
|
function affirmAuthorship(root, opts = {}) {
|
|
1983
2024
|
const withAi = opts.withAi ?? false;
|
|
1984
2025
|
const ai = withAi ? resolveAi(root, opts) : void 0;
|
|
@@ -2037,6 +2078,16 @@ function meCommand(args) {
|
|
|
2037
2078
|
process.exit(2);
|
|
2038
2079
|
return;
|
|
2039
2080
|
}
|
|
2081
|
+
if (args.includes("--print")) {
|
|
2082
|
+
const t = disclosureTrailers(root, { category: withAi ? "with_ai" : "human", tool, model });
|
|
2083
|
+
if (!t.ok) {
|
|
2084
|
+
console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
console.log(t.lines.join("\n"));
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2040
2091
|
const r = affirmAuthorship(root, { withAi, tool, model });
|
|
2041
2092
|
if (!r.ok) {
|
|
2042
2093
|
if (r.reason === "no-identity") console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
@@ -2111,6 +2162,10 @@ function aiCommand(args) {
|
|
|
2111
2162
|
return;
|
|
2112
2163
|
}
|
|
2113
2164
|
const tool = positional[0] || "AI";
|
|
2165
|
+
if (args.includes("--print")) {
|
|
2166
|
+
console.log(disclosureTrailers(root, { category: "ai", tool, model }).lines.join("\n"));
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2114
2169
|
const r = attestAiAuthorship(root, { tool, model });
|
|
2115
2170
|
if (!r.ok) {
|
|
2116
2171
|
if (r.reason === "staged-changes") {
|
|
@@ -2181,13 +2236,13 @@ function whoCommand(args) {
|
|
|
2181
2236
|
}
|
|
2182
2237
|
|
|
2183
2238
|
// src/commands/coverage.ts
|
|
2184
|
-
import { existsSync as existsSync6, readFileSync as
|
|
2239
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
2185
2240
|
import { join as join7 } from "node:path";
|
|
2186
2241
|
function policyIsRequired(root) {
|
|
2187
2242
|
const p = join7(root, ".madeby", "policy.json");
|
|
2188
2243
|
if (!existsSync6(p)) return false;
|
|
2189
2244
|
try {
|
|
2190
|
-
return parseDisclosurePolicy(JSON.parse(
|
|
2245
|
+
return parseDisclosurePolicy(JSON.parse(readFileSync8(p, "utf8"))).mode === "required";
|
|
2191
2246
|
} catch {
|
|
2192
2247
|
return false;
|
|
2193
2248
|
}
|
|
@@ -2249,9 +2304,9 @@ function coverageCommand(args) {
|
|
|
2249
2304
|
}
|
|
2250
2305
|
|
|
2251
2306
|
// src/init-repo.ts
|
|
2252
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as
|
|
2307
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2253
2308
|
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2254
|
-
var ACO_VERSION =
|
|
2309
|
+
var ACO_VERSION = 2;
|
|
2255
2310
|
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2256
2311
|
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
2257
2312
|
var ACO_OPEN_RE = /<!-- madeby:aco(?: v(\d+))? -->/;
|
|
@@ -2265,6 +2320,10 @@ After you commit, run the command for the honest category (no install, no setup)
|
|
|
2265
2320
|
- **with_ai** \u2014 a human authored it with AI assistance: \`npx madeby@latest me --with-ai [tool]\`
|
|
2266
2321
|
- **human** \u2014 a human authored it: \`npx madeby@latest me\`
|
|
2267
2322
|
|
|
2323
|
+
If your platform makes the commit for you (a cloud coding agent that pushes through its own tool),
|
|
2324
|
+
amending afterwards can be lost \u2014 put the disclosure in the message instead, with \`--print\`:
|
|
2325
|
+
\`git commit -m "<message>" -m "$(npx madeby@latest ai [tool] --print)"\`.
|
|
2326
|
+
|
|
2268
2327
|
Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
|
|
2269
2328
|
`;
|
|
2270
2329
|
var MODEL_LINE = `
|
|
@@ -2321,7 +2380,7 @@ function ensurePolicy(abs, rel, want, res) {
|
|
|
2321
2380
|
}
|
|
2322
2381
|
let raw;
|
|
2323
2382
|
try {
|
|
2324
|
-
raw = JSON.parse(
|
|
2383
|
+
raw = JSON.parse(readFileSync9(abs, "utf8"));
|
|
2325
2384
|
} catch {
|
|
2326
2385
|
res.skipped.push(rel);
|
|
2327
2386
|
return { mode: "advisory", recordModel: false, malformed: true };
|
|
@@ -2351,7 +2410,7 @@ ${block}`);
|
|
|
2351
2410
|
res.created.push(rel);
|
|
2352
2411
|
return;
|
|
2353
2412
|
}
|
|
2354
|
-
const content =
|
|
2413
|
+
const content = readFileSync9(abs, "utf8");
|
|
2355
2414
|
const status = acoStatus(content);
|
|
2356
2415
|
const loc = findAcoBlock(content);
|
|
2357
2416
|
if (status === "absent") {
|
|
@@ -2470,7 +2529,7 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
|
|
|
2470
2529
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2471
2530
|
import { createInterface } from "node:readline";
|
|
2472
2531
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2473
|
-
var SERVER_INFO = { name: "madeby", version: "0.3.
|
|
2532
|
+
var SERVER_INFO = { name: "madeby", version: "0.3.11" };
|
|
2474
2533
|
function rootFor(path) {
|
|
2475
2534
|
const cwd = path && path.length ? path : process.cwd();
|
|
2476
2535
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2544,6 +2603,7 @@ var TOOLS = [
|
|
|
2544
2603
|
category: { enum: ["human", "with_ai", "ai"], description: "the honest authorship category (see the description). Defaults from with_ai if omitted." },
|
|
2545
2604
|
with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
|
|
2546
2605
|
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." },
|
|
2606
|
+
print: { type: "boolean", description: "return the disclosure trailer lines instead of amending HEAD \u2014 for platforms that make the commit themselves; put them in the commit message's last paragraph" },
|
|
2547
2607
|
model: { type: "string", description: "the model version you are running as, e.g. claude-sonnet-5 \u2014 written as tool:model. Give it when the repo's AGENTS.md asks; omit it rather than guess." }
|
|
2548
2608
|
}
|
|
2549
2609
|
},
|
|
@@ -2552,6 +2612,10 @@ var TOOLS = [
|
|
|
2552
2612
|
const tool = args.tool;
|
|
2553
2613
|
const model = args.model;
|
|
2554
2614
|
const category = args.category;
|
|
2615
|
+
if (args.print === true) {
|
|
2616
|
+
const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
|
|
2617
|
+
return disclosureTrailers(root, { category: c, tool, model });
|
|
2618
|
+
}
|
|
2555
2619
|
if (category === "ai") return attestAiAuthorship(root, { tool, model });
|
|
2556
2620
|
const withAi = category === "with_ai" || args.with_ai === true;
|
|
2557
2621
|
return affirmAuthorship(root, { withAi, tool, model: withAi ? model : void 0 });
|
|
@@ -2793,6 +2857,8 @@ Usage:
|
|
|
2793
2857
|
madeby me --with-ai [tool] Made by me, with AI: also disclose AI assistance (Assisted-by:
|
|
2794
2858
|
<tool>, default "AI") \u2014 the assisted middle state, classified with_ai.
|
|
2795
2859
|
--model <version> names the model too (Assisted-by: <tool>:<version>).
|
|
2860
|
+
--print (me / ai) prints the trailer lines instead of amending HEAD,
|
|
2861
|
+
for the commit message itself: git commit -m <msg> -m "$(madeby ai <tool> --print)".
|
|
2796
2862
|
madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
|
|
2797
2863
|
madeby help Show this help.
|
|
2798
2864
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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",
|