Repo
Repos: git repositories for the code your agents and apps write.
repo = noclick.Repo.from_name("agent-tools", create_if_missing=True)
repo.commit({"src/index.ts": source}, message="Add the lookup route")
print(repo.read("src/index.ts").decode())
print(repo.clone_url()) # plain git over HTTPS, a short-lived token insideA repo is plain git: clone it, push to it, or move it to GitHub whole.
A project (noclick.Project) deploys one to a URL.
class RepoA git repository NoClick hosts. Repo.from_name("agent-tools", create_if_missing=True) is lazy; commit to it from code, or clone it and
push with plain git.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | |
| default_branch | str | The branch a project deploys to production ( |
| size_bytes | int | None | |
| last_commit | Commit | None | The newest commit on the default branch (None in an empty repo). |
| used_by | list[dict[str, Any]] | The apps that deploy it: |
| user_id | str | None | |
| created_at | Any |
createclassmethodasync
noclick.Repo.create(name, *, default_branch=None, user=..., client=None) -> RepoCreate an empty repo called name (unique per user; a duplicate
raises noclick.Conflict with existing_id).
NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new repo, loaded, with no commits yet.
Create one for your account's own code:
repo = noclick.Repo.create("agent-tools")
print(repo.name, repo.default_branch) # agent-tools mainInside as_user it's that user's, and bills to them:
with noclick.as_user("user_42"):
site = noclick.Repo.create("portfolio", default_branch="production")from_nameclassmethod
noclick.Repo.from_name(
name, *, create_if_missing=False, default_branch=None, user=..., client=None
) -> RepoA lazy reference to the repo called name in the current (or
given) user. With create_if_missing=True it's created, empty, on
first use; an existing repo is used as it is.
FalseNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A repo that loads, or is created, the first time it's used. Nothing is sent yet.
The same line works on the first run and every run after:
repo = noclick.Repo.from_name("agent-tools", create_if_missing=True)
repo.commit({"README.md": "# Agent tools\n"}, message="Start the repo")Without create_if_missing a missing repo raises noclick.NotFound at first use:
repo = noclick.Repo.from_name("booking", user="acme")
print(repo.last_commit)from_idclassmethod
noclick.Repo.from_id(repo_id, *, client=None, user=...) -> RepoA lazy reference to a repo by id.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
A repo that loads the first time you read a field. Nothing is sent yet.
repo = noclick.Repo.from_id("c4e1a7b2-5d3f-4a8e-9b6c-1f2d3e4a5b60")
for branch in repo.branches():
print(branch.name, branch.head)getclassmethodasync
noclick.Repo.get(repo_id, *, user=..., client=None) -> RepoLoad a repo by id now; an unknown id raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The repo, loaded.
repo = noclick.Repo.get("c4e1a7b2-5d3f-4a8e-9b6c-1f2d3e4a5b60")
print(repo.name, repo.size_bytes, repo.last_commit)listclassmethodasync
noclick.Repo.list(*, limit=None, user_filter=None, user=..., client=None) -> list[Repo]The current user's repos (the account's own outside as_user, or those user_filter covers).
Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching repo, newest first, fetched page by page.
for repo in noclick.Repo.list():
print(repo.name, repo.default_branch, repo.size_bytes)
everyone = noclick.Repo.list(user_filter=noclick.ALL_USERS) # every user's, plus your account'slist_pageclassmethodasync
noclick.Repo.list_page(
*, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> PageOne page of repos; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the repos and whose next_cursor is None on the last page.
page = noclick.Repo.list_page(limit=50)
while True:
for repo in page.data:
print(repo.name)
if page.next_cursor is None:
break
page = noclick.Repo.list_page(limit=50, cursor=page.next_cursor)clone_urlasync
repo.clone_url(*, write=False, ttl_s=None) -> strA git URL with a short-lived token inside, for git clone,
fetch and, with write=True, push. ttl_s is 3,600 by
default, at most 86,400; the token stops working after it. The token
reaches this repo only. Clones and pushes cost nothing extra, within fair
use. An agent's writing URL (asked for through NoClick's MCP server, by
the coordinator or by a platform agent) goes through NoClick, which never
lets it force-push or delete the default branch, and keeps the old head of
any other branch its push rewrites or deletes (backups).
FalseNoneAn https:// URL that plain git accepts as it is. Treat it as a secret until it expires.
Clone the repo on a build machine:
import subprocess
repo = noclick.Repo.from_name("agent-tools")
subprocess.run(["git", "clone", repo.clone_url(), "agent-tools"], check=True)Let a sandbox push for the next ten minutes:
repo = noclick.Repo.from_name("agent-tools")
url = repo.clone_url(write=True, ttl_s=600)
with noclick.Sandbox.create(network=True) as sb:
sb.exec(["git", "clone", url, "/workspace/agent-tools"])checkoutinvite-only beta
repo.checkout(*, branch=None, path=None, push=None) -> RepoCheckoutThis repo checked out in an agent's workspace, for Agent(repos=[...]):
cloned into /workspace/ (the repo's name when omitted) at a
conversation's first turn, on branch (the default branch when omitted).
push is the branches the agent may push to, exact or prefix/*;
without it the checkout is read-only. Sends nothing.
NoneNoneNoneA RepoCheckout to list in an agent's repos.
Let an agent change the site on branches of its own, for review:
site = noclick.Repo.from_name("booking")
builder = noclick.Agent.from_name(
"site-builder",
create_if_missing=True,
instructions="You make the changes asked for in /workspace/booking, on a new branch, and push it.",
repos=[site.checkout(push="agent/*")],
)commitasync
repo.commit(files, *, message, branch=None, author=None, exclude=()) -> Commit | NoneCommit files in one commit: each path maps to its new content
(text, or bytes for binary files) or to None to delete it. The commit
goes on branch (the default branch when omitted), which is created
from the default branch's head if it doesn't exist yet. At most 1,000
files and 50 MB in one commit.
files may instead be a folder on disk: the branch's tree is made to match
it, and only what changed is sent (new and changed files written, files the
folder no longer has deleted), so running it again after no change commits
nothing. .git is skipped, and so is any path matching one of the
exclude globs (["node_modules", "*.log"]), which also keeps the repo's
own copies of those paths.
NoneNone()The new commit, with its sha; for a folder with nothing changed, None.
Write two files and delete one, as one commit:
repo = noclick.Repo.from_name("agent-tools", create_if_missing=True)
commit = repo.commit(
{"src/index.ts": "export const lookup = () => 42;\n", "package.json": '{"name": "agent-tools"}', "old.txt": None},
message="Add the lookup route",
)
print(commit.sha, commit.files_changed)Commit to a branch of its own, for a preview deployment, and name the author:
repo = noclick.Repo.from_name("booking")
repo.commit(
{"app/routes/book.tsx": "export default function Book() { return null; }\n"},
message="Try the new booking form",
branch="new-form",
author="support-bot <support-bot@agents.noclick.app>",
)Commit a folder a build wrote, sending only what changed since the last run:
site = noclick.Repo.from_name("marketing-site", create_if_missing=True)
commit = site.commit("./dist", message="Rebuild the site", exclude=["*.map"])
print(commit.sha if commit else "nothing changed")readasync
repo.read(path, *, ref=None) -> bytesRead one file at ref (a branch, tag or commit sha; the default
branch's head when omitted). A missing path raises noclick.NotFound.
NoneThe file's bytes exactly as committed; decode text yourself.
repo = noclick.Repo.from_name("booking")
manifest = repo.read("package.json", ref="main").decode()
print(manifest)filesasync
repo.files(prefix='', *, ref=None, recursive=False) -> list[RepoFile]What's in the directory prefix (the root when empty) at ref.
recursive=True lists every file below it instead, without the directories.
''NoneFalseIts files and directories, directories first, each by path from the repo's root.
Walk the tree one level at a time:
repo = noclick.Repo.from_name("booking")
for entry in repo.files("app/", ref="main"):
print(entry.type, entry.path, entry.size)Or read every file under a directory:
repo = noclick.Repo.from_name("booking")
for entry in repo.files("app/routes/", recursive=True):
print(entry.path, len(repo.read(entry.path)))branchesasync
repo.branches() -> list[Branch]The repo's branches, the default one first.
Each branch with its head sha and when it last moved.
repo = noclick.Repo.from_name("booking")
for branch in repo.branches():
print(branch.name, branch.head, branch.updated_at)backupsasync
repo.backups(*, branch=None, limit=50) -> list[RepoBackup]The branches agents' pushes rewrote or deleted, as NoClick kept them first: newest first, each for 30 days. Through NoClick an agent never force-pushes or deletes the default branch at all.
NoneReturn at most this many.
50)Up to limit backups (None for all of them), of branch alone when it's given.
repo = noclick.Repo.from_name("booking")
for backup in repo.backups(branch="agent/new-form"):
print(backup.reason, backup.sha[:7], backup.created_at)restore_backupasync
repo.restore_backup(backup) -> BranchMake a backup's branch again at the commit it was kept at (backup: a RepoBackup or its id).
When the repo has a branch by that name again, the backup comes back as <branch>-restored-<time>
instead; nothing that's there moves.
The branch made, with its head.
repo = noclick.Repo.from_name("booking")
[latest] = repo.backups(branch="agent/new-form", limit=1)
print(repo.restore_backup(latest).name)commitsasync
repo.commits(*, branch=None, limit=20) -> list[Commit]The history of branch (the default branch when omitted), newest first.
NoneReturn at most this many.
20)Up to limit commits (20 by default; None for the whole history), fetched page by page.
repo = noclick.Repo.from_name("booking")
for commit in repo.commits(branch="main", limit=5):
print(commit.sha[:7], commit.author, commit.message)commits_pageasync
repo.commits_page(*, branch=None, cursor=None, limit=None) -> PageOne page of commits, newest first; pass next_cursor back as cursor.
NoneThe next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)repo = noclick.Repo.from_name("booking")
page = repo.commits_page(branch="main", limit=50)
while True:
for commit in page.data:
print(commit.sha[:7], commit.message)
if page.next_cursor is None:
break
page = repo.commits_page(branch="main", limit=50, cursor=page.next_cursor)diffasync
repo.diff(sha) -> CommitDiffWhat the commit sha (full or short) changed against its first parent. An unknown commit
raises noclick.NotFound.
The commit with each file it changed, the file's status, the lines it added and deleted and its patch in git's unified format.
repo = noclick.Repo.from_name("booking")
[latest] = repo.commits(limit=1)
changes = repo.diff(latest.sha)
print(f"+{changes.additions} -{changes.deletions}")
for file in changes.files:
print(file.status, file.path)
print(file.patch or "(not shown)")exportasync
repo.export() -> OperationPackage the repo to take elsewhere: every branch and its history as a git bundle.
An operation whose .wait() gives an noclick.ExportBundle: a zip at url
(a link that expires) holding the bundle, which git clone reads as it is.
repo = noclick.Repo.from_name("agent-tools")
bundle = repo.export().wait()
print(bundle.url, bundle.expires_at)export_to_githubasync
repo.export_to_github(*, connection, owner, name=None, private=True) -> OperationCopy the repo, every branch and tag with its history, into a new
GitHub repository owner/name (name defaults to this repo's),
through connection: a GitHub connection (a Connection or its
name) of the repo's user, or of the account. owner is the connected
account's own login or an organization it may create repositories in.
The NoClick repo stays as it was.
A connection with no GitHub account connected yet raises
noclick.NeedsConnection, whose connect_url connects it. The
operation fails with noclick.Conflict when GitHub already has
owner/name, and noclick.PermissionDenied when the connected
account can't create repositories there.
NoneTrueAn operation whose .wait() gives the new GitHub repository's URL.
Hand a repo to its owner's GitHub organization:
repo = noclick.Repo.from_name("agent-tools")
try:
url = repo.export_to_github(connection="github", owner="acme").wait()
print(url) # https://github.com/acme/agent-tools
except noclick.NeedsConnection as needed:
print("Connect GitHub first:", needed.connect_url)deleteasync
repo.delete() -> TrashItemDelete the repo and its history: it goes to the trash, restorable for 30 days. A repo an app deploys
from (see used_by) is refused with noclick.Conflict; delete that app first.
repo = noclick.Repo.from_name("scratch")
repo.delete()Commit
class CommitOne commit: its sha, message and author, the branch it was
made on, and files_changed, how many files it added, changed or deleted.
| sha | str | |
| message | str | |
| author | str | None | |
| branch | str | None | |
| created_at | Any | |
| files_changed | int | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
CommitDiff
class CommitDiffWhat one commit changed against its first parent: the commit, its parents (none for the
first commit), the lines it added and deleted in all, and each file it changed.
| commit | Commit | |
| parents | list[str] | |
| additions | int | |
| deletions | int | |
| files | list[FileChange] |
FileChange
class FileChangeOne file a commit changed: its path (and old_path when it was renamed), its status
(added, modified, deleted, renamed, copied or type_changed), the lines it added
and deleted, its size in bytes, and patch, its unified diff in git's format. patch (and the
line counts) are None for a file that isn't shown: one that isn't text, or is too large.
| path | str | |
| status | str | |
| additions | int | None | |
| deletions | int | None | |
| size | int | None | |
| patch | str | None | |
| old_path | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Branch
class BranchA branch: its name, the sha of its head commit, and when it last moved.
| name | str | |
| head | str | None | |
| updated_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
RepoBackup
class RepoBackupA branch's old head NoClick kept before an agent's push rewrote it (reason "rewritten": a
force-push) or deleted it ("deleted"): the branch, the commit sha it was at, the platform agent
whose push it was (agent_id, else None), and when it was kept. It's kept 30 days, until
expires_at, out of the branch list and clones; Repo.restore_backup makes the branch again.
| id | str | |
| branch | str | |
| sha | str | |
| reason | str | |
| agent_id | str | None | |
| created_at | Any | |
| expires_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
RepoFile
class RepoFileAn entry in a repo's tree: its path from the root, its type
(file or dir) and, for a file, its size in bytes and sha, git's
blob id for its content (it changes whenever the content does).
| path | str | |
| type | str | |
| size | int | None | |
| sha | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
RepoCheckout
class RepoCheckoutA git repository an agent works in, for Agent(repos=[...]): a
connection's repository (connection.checkout("acme/docs")) or a NoClick
repo (repo.checkout(); invite-only beta). NoClick clones it into
/workspace/ (the repository's name when path is omitted) at a
conversation's first turn, and the agent's git reaches that repository only,
through NoClick, with a key made for its sandbox; the connection's own token
never enters it. push lists the branches it may push to: exact names, or a
prefix ending in /* ("noclick/*" reaches noclick/fix and
noclick/a/b); NoClick's git endpoint refuses any other push, and with no
push the checkout is read-only. Whatever push allows, the agent can't
force-push or delete the repository's default branch, and before its push
rewrites or deletes any other branch NoClick keeps the old head as a backup
for 30 days (on a git host, refs/noclick/backups/<branch>/<time> in the
repository; push it back to restore it). A policy can't do this job: a coding
harness runs git push in its own shell, which is no tool call for a
policy to judge. gh only reads the repository there.
| repository | str | None |
|
| connection | Any | |
| repo | Any | |
| branch | str | None | The branch it checks out (the repository's default when None). |
| path | str | None | The folder under |
| push | list[str] | None | Branches it may push to, exact or |