@quilt-dev/cli
Advanced tools
| 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 |
+9
-1
@@ -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): " + |
+9
-7
@@ -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 |
+6
-2
@@ -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 |
+20
-9
@@ -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, |
+4
-3
| { | ||
| "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": [ |
+6
-1
@@ -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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
445786
2.27%33
3.13%9461
2.23%233
2.19%