🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@quilt-dev/cli

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@quilt-dev/cli - npm Package Compare versions

Comparing version
0.5.1
to
0.5.2
+137
dist/provenance.js
import { git } from "./git.js";
export const PROVENANCE_TRAILER = "Quilt-Provenance";
function parseHunks(patch, fileCount) {
const chunks = patch.split(/^diff --git /m).slice(1);
const byFile = [];
for (let fileIndex = 0; fileIndex < fileCount; fileIndex++) {
const hunks = [];
for (const line of (chunks[fileIndex] ?? "").split("\n")) {
const header = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
if (!header)
continue;
hunks.push({
oldStart: Number(header[1]),
oldLines: header[2] === undefined ? 1 : Number(header[2]),
newStart: Number(header[3]),
newLines: header[4] === undefined ? 1 : Number(header[4]),
});
}
byFile.push(hunks);
}
return byFile;
}
/** Build the portable v1 record. A Quilt commit is single-actor by
* construction, and the hunk ranges name exactly which parts of its diff that
* actor committed. Prompt correlation remains local until capture events carry
* an exact, compaction-safe source-session link. */
export function buildCommitProvenance(selection, actor, sessionId, includeUnclaimed = false) {
const parsed = parseHunks(selection.patch, selection.files.length);
const files = selection.files.map((file, fileIndex) => ({
path: file.path,
hunks: parsed[fileIndex] ?? [],
}));
for (const path of selection.wholeFiles)
files.push({ path, hunks: [] });
return {
version: 1,
actor: { id: actor.id, type: actor.type, displayName: actor.displayName },
sessionId,
capture: includeUnclaimed ? "owned+unclaimed" : "owned",
tree: null,
parent: null,
files,
};
}
export function encodeProvenance(value) {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
}
export function commitMessageWithProvenance(message, value) {
// Quilt owns this trailer namespace. Remove user-supplied copies so an
// earlier forged record cannot disagree with the canonical final trailers.
const clean = message
.split("\n")
.filter((line) => !/^Quilt-(?:Actor|Session|Capture|Provenance):/i.test(line))
.join("\n")
.trimEnd();
const trailerValue = (input) => input.replace(/[\r\n]+/g, " ").trim();
const trailers = [
`Quilt-Actor: ${trailerValue(value.actor.id)}`,
...(value.sessionId ? [`Quilt-Session: ${trailerValue(value.sessionId)}`] : []),
`Quilt-Capture: ${value.capture}`,
`${PROVENANCE_TRAILER}: ${encodeProvenance(value)}`,
];
return clean + "\n\n" + trailers.join("\n") + "\n";
}
function isNonNegativeInteger(value) {
return typeof value === "number" && Number.isInteger(value) && value >= 0;
}
function isObjectId(value) {
return typeof value === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value);
}
function isProvenance(value) {
if (!value || typeof value !== "object")
return false;
const record = value;
const actor = record.actor;
if (record.version !== 1 || !actor || typeof actor !== "object")
return false;
if (typeof actor.id !== "string" || typeof actor.displayName !== "string")
return false;
if (actor.type !== "human" && actor.type !== "agent" && actor.type !== "bot")
return false;
if (record.sessionId !== null && typeof record.sessionId !== "string")
return false;
if (record.capture !== "owned" && record.capture !== "owned+unclaimed")
return false;
if (!isObjectId(record.tree))
return false;
if (record.parent !== null && !isObjectId(record.parent))
return false;
if (!Array.isArray(record.files))
return false;
return record.files.every((file) => {
if (!file || typeof file !== "object")
return false;
const entry = file;
if (typeof entry.path !== "string" || !Array.isArray(entry.hunks))
return false;
return entry.hunks.every((hunk) => {
if (!hunk || typeof hunk !== "object")
return false;
const range = hunk;
return isNonNegativeInteger(range.oldStart) && isNonNegativeInteger(range.oldLines) &&
isNonNegativeInteger(range.newStart) && isNonNegativeInteger(range.newLines);
});
});
}
export function decodeProvenance(encoded) {
try {
const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
return isProvenance(value) ? value : null;
}
catch {
return null;
}
}
/** Read and verify provenance from a commit message. The record travels with
* ordinary Git pushes and merges, with no separate notes ref to configure. */
export function readCommitProvenance(repoRoot, revision = "HEAD") {
if (revision.startsWith("-"))
return null;
const shown = git(["show", "-s", "--format=%B", revision], { cwd: repoRoot, check: false });
if (shown.status !== 0)
return null;
const matches = [...shown.stdout.matchAll(/^Quilt-Provenance:\s*(\S+)\s*$/gm)];
const encoded = matches.at(-1)?.[1];
const record = encoded ? decodeProvenance(encoded) : null;
if (!record)
return null;
const objects = git(["show", "-s", "--format=%T%n%P", revision], { cwd: repoRoot, check: false });
if (objects.status !== 0)
return null;
const [tree, parents = ""] = objects.stdout.trimEnd().split("\n");
const firstParent = parents.trim().split(/\s+/).filter(Boolean)[0] ?? null;
if (record.tree !== tree || record.parent !== firstParent)
return null;
return record;
}
+5
-5

@@ -7,2 +7,3 @@ import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";

import { symbolLocator, opKeyer, OWN_KEY_SEP } from "./symbols.js";
import { commitMessageWithProvenance } from "./provenance.js";
/** git mode for a working-tree file: 100755 if executable, else 100644. */

@@ -255,4 +256,2 @@ function worktreeMode(repoRoot, relPath) {

if (built.added === 0 && built.removed === 0) {
if (built.hasOther)
blockedFiles.push(file.path);
continue;

@@ -280,4 +279,2 @@ }

totalRemoved += built.removed;
if (built.hasOther)
blockedFiles.push(file.path);
}

@@ -368,3 +365,6 @@ return {

const parentArgs = base ? ["-p", base] : [];
const commitSha = git(["commit-tree", tree, ...parentArgs, "-m", message], { cwd: repoRoot, env: identityEnv }).stdout.trim();
const commitMessage = opts.provenance
? commitMessageWithProvenance(message, { ...opts.provenance, tree, parent: base })
: message;
const commitSha = git(["commit-tree", tree, ...parentArgs, "-F", "-"], { cwd: repoRoot, env: identityEnv, input: commitMessage }).stdout.trim();
// Move the branch with a compare-and-swap on the old value. If another actor

@@ -371,0 +371,0 @@ // committed in the meantime, the CAS fails and we surface a retry instead of

@@ -16,2 +16,3 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { repoRelative } from "./paths.js";
import { buildCommitProvenance } from "./provenance.js";
/**

@@ -296,4 +297,7 @@ * The Quilt MCP server (stdio). Attribution is per-agent. Two ways to identify:

}
const sessionId = active?.actorId === actorId ? active.session?.id ?? null : null;
const provenance = buildCommitProvenance(sel, actor, sessionId, includeUnclaimed);
const res = commitSelection(repoRoot, sel, actor, message, {
defaultAuthorEmail: store.readConfig()?.defaultAuthorEmail,
provenance,
});

@@ -327,3 +331,7 @@ let releasedClaims = 0;

skippedUnowned: sel.skippedUnowned,
note: "committed files' claims were auto-released — no separate release call needed" +
provenance: {
version: provenance.version,
capture: provenance.capture,
},
note: "committed files' claims were auto-released; durable provenance was embedded in the Git commit" +
(sel.skippedBinary.length

@@ -330,0 +338,0 @@ ? "; WARNING: unclaimed binary/too-large files were SKIPPED (claim them to commit them whole): " +

@@ -34,3 +34,3 @@ // Magical onboarding: detect the agent orchestrator in a repo and wire Quilt in

*/
export const COORDINATION_VERSION = 2;
export const COORDINATION_VERSION = 3;
export const COORDINATION_MARKER = `<!-- quilt:coordination v${COORDINATION_VERSION} -->`;

@@ -51,8 +51,10 @@ /** Closes the block so a future refresh can replace exactly the marked region. */

You share this checkout with other agents. Quilt protects your work
You share this checkout with other agents. Quilt captures who changed what
automatically:
- Your edits are captured and protected by the quilt hooks: nothing to
approve, nothing to call. Identity is automatic (each session gets its own
id), and every line you edit is attributed to you as you write it.
- Your edits are captured by the quilt hooks: nothing to call. Identity is
automatic (each session gets its own id), and every line you edit is
attributed to you as you write it. Claude Code hooks also deny edits into
claimed code. Codex hooks are capture-only, so use claim-aware MCP tools when
you need prevention there.
- To commit only your lines, run \`quilt commit --mine -m "<message>"\` from

@@ -64,4 +66,4 @@ the shell. It works with or without the MCP server, and it leaves everyone

approved in your client. If the quilt tools are NOT in your MCP list you
are still protected: capture and attribution run in the hooks. Just commit
with the CLI.
still keep capture and attribution through the hooks. Just commit with the
CLI. Codex still needs MCP tools for claim enforcement.

@@ -68,0 +70,0 @@ Optional, when the quilt MCP tools are connected (CLI equivalents in

@@ -11,4 +11,8 @@ /**

const sessionId = envSession ?? store.readCurrentSessionId();
const session = sessionId ? store.readSession(sessionId) : null;
const actorId = envActor ?? session?.actorId ?? null;
const foundSession = sessionId ? store.readSession(sessionId) : null;
const actorId = envActor ?? foundSession?.actorId ?? null;
// An explicit actor must never inherit another actor's checkout-global
// session. Besides confusing whoami, that would attach the wrong session and
// prompt lineage to a durable provenance record.
const session = envActor && foundSession?.actorId !== envActor ? null : foundSession;
const actor = actorId ? store.findActor(actorId) : null;

@@ -15,0 +19,0 @@ const source = envActor

@@ -1,2 +0,2 @@

import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, appendFileSync, openSync, closeSync, statSync, rmSync, } from "node:fs";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, appendFileSync, openSync, closeSync, statSync, rmSync, renameSync, } from "node:fs";
import { join } from "node:path";

@@ -15,3 +15,5 @@ import { QuiltPaths } from "./paths.js";

function writeJson(file, value) {
writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
const tmp = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
renameSync(tmp, file);
}

@@ -48,9 +50,18 @@ /** Reads and writes everything under .quilt/ for a repo. */

upsertActor(actor) {
const actors = this.readActors();
const idx = actors.findIndex((a) => a.id === actor.id);
if (idx >= 0)
actors[idx] = actor;
else
actors.push(actor);
writeJson(this.paths.actors, { actors });
this.withLock(() => {
const actors = this.readActors();
const idx = actors.findIndex((a) => a.id === actor.id);
if (idx >= 0) {
const existing = actors[idx];
actors[idx] = {
...existing,
...actor,
createdAt: existing.createdAt,
email: actor.email ?? existing.email,
};
}
else
actors.push(actor);
writeJson(this.paths.actors, { actors });
});
}

@@ -57,0 +68,0 @@ findActor(id) {

@@ -12,2 +12,6 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";

const POSTHOG_KEY = "phc_7aGIe4BBqxv2qxtftFdi83so2t5LrV0UBFqZaYsOz9w";
/** Process-scoped fallback when QUILT_TELEMETRY=1 opts in without a stored
* decision. It avoids grouping unrelated installs without writing consent
* state or minting a different identity for every event in one process. */
const ephemeralAnonymousId = randomUUID();
/** Config dir override for tests; XDG-respecting default otherwise. */

@@ -66,4 +70,5 @@ function configDir() {

event,
distinct_id: readTelemetryConfig()?.anonymousId ?? "undecided",
distinct_id: readTelemetryConfig()?.anonymousId ?? ephemeralAnonymousId,
properties: {
$process_person_profile: false,
quilt_version: VERSION,

@@ -70,0 +75,0 @@ platform: process.platform,

{
"name": "@quilt-dev/cli",
"version": "0.5.1",
"version": "0.5.2",
"mcpName": "io.github.wkoverfield/quilt",

@@ -34,5 +34,6 @@ "description": "Actor-owned patches for Git. Same repo. Many agents. Clean commits.",

"start": "node dist/cli.js",
"test": "npm run build && QUILT_CODEX_DIR=$PWD/.no-codex-in-tests QUILT_NO_UPDATE_CHECK=1 node --import tsx --test test/*.test.ts",
"check:release": "node scripts/check-release-version.mjs",
"test": "npm run check:release && npm run build && QUILT_CODEX_DIR=$PWD/.no-codex-in-tests QUILT_NO_UPDATE_CHECK=1 node --import tsx --test test/*.test.ts",
"bench": "npm run build && node --import tsx bench/run.ts",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run check:release && npm run build"
},

@@ -39,0 +40,0 @@ "keywords": [

@@ -23,3 +23,3 @@ # Quilt

npm install -g @quilt-dev/cli
quilt setup # capture hooks wired, protection live (MCP tools optional, on top)
quilt setup # capture hooks wired; claim enforcement via Claude hooks or MCP
```

@@ -152,4 +152,9 @@

quilt commit --mine -m "fix auth redirect"
quilt provenance HEAD # actor, session, files, hunks, tree
```
Quilt writes that provenance into the Git commit itself, so it survives normal
pushes and fresh clones. Prompt correlation stays local in `quilt ui`; it is not
published into Git history.
In a shared shell, make the committer explicit (`quilt --as auth-agent commit

@@ -156,0 +161,0 @@ --mine ...`). Quilt refuses a checkout-global session identity when the dirty

Sorry, the diff of this file is too big to display