Skip to content

Commit 431fa10

Browse files
napoleondclaude
andcommitted
feat(git): add git repository hosting command and skill
Adds `atxp git` CLI command (create, list, remote-url, delete) backed by the git.mcp.atxp.ai MCP server. Includes a separate atxp-git skill with documentation emphasizing the ephemeral nature of authenticated remote URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d548dc commit 431fa10

5 files changed

Lines changed: 293 additions & 2 deletions

File tree

packages/atxp/src/commands/git.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'git.mcp.atxp.ai';
5+
6+
export interface GitOptions {
7+
writable?: boolean;
8+
ttl?: number;
9+
defaultBranch?: string;
10+
visibility?: string;
11+
limit?: number;
12+
cursor?: string;
13+
}
14+
15+
export async function gitCommand(
16+
subCommand: string,
17+
options: GitOptions,
18+
positionalArg?: string
19+
): Promise<void> {
20+
switch (subCommand) {
21+
case 'create': {
22+
if (!positionalArg) {
23+
console.error(chalk.red('Error: Repository name is required'));
24+
console.log(`Usage: ${chalk.cyan('npx atxp git create <repoName> [--visibility public|private] [--default-branch <branch>]')}`);
25+
process.exit(1);
26+
}
27+
const args: Record<string, unknown> = { repoName: positionalArg };
28+
if (options.visibility) args.visibility = options.visibility;
29+
if (options.defaultBranch) args.defaultBranch = options.defaultBranch;
30+
const result = await callTool(SERVER, 'git_create_repo', args);
31+
console.log(result);
32+
break;
33+
}
34+
35+
case 'list': {
36+
const args: Record<string, unknown> = {};
37+
if (options.limit) args.limit = options.limit;
38+
if (options.cursor) args.cursor = options.cursor;
39+
const result = await callTool(SERVER, 'git_list_repos', args);
40+
console.log(result);
41+
break;
42+
}
43+
44+
case 'remote-url': {
45+
if (!positionalArg) {
46+
console.error(chalk.red('Error: Repository name is required'));
47+
console.log(`Usage: ${chalk.cyan('npx atxp git remote-url <repoName> [--writable] [--ttl <seconds>]')}`);
48+
process.exit(1);
49+
}
50+
const args: Record<string, unknown> = { repoName: positionalArg };
51+
if (options.writable) args.writable = true;
52+
if (options.ttl) args.ttlSeconds = options.ttl;
53+
const result = await callTool(SERVER, 'git_get_remote_url', args);
54+
console.log(result);
55+
break;
56+
}
57+
58+
case 'delete': {
59+
if (!positionalArg) {
60+
console.error(chalk.red('Error: Repository name is required'));
61+
console.log(`Usage: ${chalk.cyan('npx atxp git delete <repoName>')}`);
62+
process.exit(1);
63+
}
64+
const result = await callTool(SERVER, 'git_delete_repo', { repoName: positionalArg });
65+
console.log(result);
66+
break;
67+
}
68+
69+
case 'help':
70+
case '':
71+
console.log(chalk.bold('Git Repository Management'));
72+
console.log();
73+
console.log(chalk.bold('Usage:'));
74+
console.log(` npx atxp git ${chalk.yellow('<command>')} [options]`);
75+
console.log();
76+
console.log(chalk.bold('Commands:'));
77+
console.log(` ${chalk.cyan('create')} ${chalk.yellow('<repoName>')} Create a new repository ($0.50)`);
78+
console.log(` ${chalk.cyan('list')} List your repositories (free)`);
79+
console.log(` ${chalk.cyan('remote-url')} ${chalk.yellow('<repoName>')} Get an authenticated clone/push URL`);
80+
console.log(` ${chalk.cyan('delete')} ${chalk.yellow('<repoName>')} Delete a repository (free)`);
81+
console.log(` ${chalk.cyan('help')} Show this help message`);
82+
console.log();
83+
console.log(chalk.bold('Options:'));
84+
console.log(` ${chalk.yellow('--writable')} Request a writable URL ($0.01, default: read-only/free)`);
85+
console.log(` ${chalk.yellow('--ttl')} ${chalk.yellow('<seconds>')} URL expiry in seconds (default: 3600)`);
86+
console.log(` ${chalk.yellow('--visibility')} ${chalk.yellow('<mode>')} private or public (default: private)`);
87+
console.log(` ${chalk.yellow('--default-branch')} ${chalk.yellow('<name>')} Default branch name (default: main)`);
88+
console.log(` ${chalk.yellow('--limit')} ${chalk.yellow('<n>')} Max repos to list (default: 20)`);
89+
console.log(` ${chalk.yellow('--cursor')} ${chalk.yellow('<token>')} Pagination cursor for list`);
90+
console.log();
91+
console.log(chalk.bold('Examples:'));
92+
console.log(' npx atxp git create my-project');
93+
console.log(' npx atxp git create my-project --visibility public');
94+
console.log(' npx atxp git list');
95+
console.log(' npx atxp git remote-url my-project --writable');
96+
console.log(' npx atxp git delete my-project');
97+
console.log();
98+
console.log(chalk.bold(chalk.yellow('Important: Remote URLs are ephemeral')));
99+
console.log(' URLs from remote-url contain a time-limited JWT (default: 1 hour).');
100+
console.log(' When expired, git operations return an auth error. Get a fresh URL:');
101+
console.log(` ${chalk.cyan('npx atxp git remote-url my-project --writable')}`);
102+
console.log(' Then update the remote:');
103+
console.log(` ${chalk.cyan('git remote set-url origin <new-url>')}`);
104+
break;
105+
106+
default:
107+
console.error(chalk.red(`Unknown git command: ${subCommand}`));
108+
console.log(`Run ${chalk.cyan('npx atxp git help')} for available commands.`);
109+
process.exit(1);
110+
}
111+
}

packages/atxp/src/help.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function showHelp(): void {
3131
console.log(' ' + chalk.cyan('agent') + ' ' + chalk.yellow('<command>') + ' ' + 'Create and manage agent accounts');
3232
console.log(' ' + chalk.cyan('memory') + ' ' + chalk.yellow('<command>') + ' ' + 'Manage, search, and back up agent memory files');
3333
console.log(' ' + chalk.cyan('contacts') + ' ' + chalk.yellow('<command>') + '' + 'Manage local contacts with cloud backup');
34+
console.log(' ' + chalk.cyan('git') + ' ' + chalk.yellow('<command>') + ' ' + 'Git repository hosting (create, clone, push)');
3435
console.log(' ' + chalk.cyan('notifications') + ' ' + chalk.yellow('enable') + ' ' + 'Enable push notifications');
3536
console.log(' ' + chalk.cyan('transactions') + ' ' + chalk.yellow('[options]') + ' ' + 'View recent transaction history');
3637
console.log();
@@ -144,6 +145,14 @@ export function showHelp(): void {
144145
console.log(' npx atxp contacts pull');
145146
console.log();
146147

148+
console.log(chalk.bold('Git Examples:'));
149+
console.log(' npx atxp git create my-project # Create a repo ($0.50)');
150+
console.log(' npx atxp git list # List your repos');
151+
console.log(' npx atxp git remote-url my-project # Get a read-only clone URL');
152+
console.log(' npx atxp git remote-url my-project --writable # Get a push-capable URL ($0.01)');
153+
console.log(' npx atxp git delete my-project # Delete a repo');
154+
console.log();
155+
147156
console.log(chalk.bold('PAAS Examples:'));
148157
console.log(' npx atxp paas worker deploy my-api --code ./worker.js');
149158
console.log(' npx atxp paas db create my-database');

packages/atxp/src/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { agentCommand } from './commands/agent.js';
2121
import { whoamiCommand } from './commands/whoami.js';
2222

2323
import { memoryCommand, type MemoryOptions } from './commands/memory.js';
24+
import { gitCommand, type GitOptions } from './commands/git.js';
2425
import { contactsCommand } from './commands/contacts.js';
2526
import { transactionsCommand } from './commands/transactions.js';
2627
import { notificationsCommand } from './commands/notifications.js';
@@ -118,14 +119,15 @@ function parseArgs(): {
118119
musicOptions: MusicOptions;
119120
searchOptions: SearchOptions;
120121
memoryOptions: MemoryOptions;
122+
gitOptions: GitOptions;
121123
contactsOptions: ContactsOptionsLocal;
122124
} {
123125
const command = process.argv[2];
124126
const subCommand = process.argv[3];
125127

126128
// Check for help flags early - but NOT for paas or email commands (they handle --help internally)
127129
const helpFlag = process.argv.includes('--help') || process.argv.includes('-h');
128-
if (helpFlag && command !== 'paas' && command !== 'email' && command !== 'phone' && command !== 'agent' && command !== 'fund' && command !== 'deposit' && command !== 'memory' && command !== 'backup' && command !== 'contacts' && command !== 'notifications') {
130+
if (helpFlag && command !== 'paas' && command !== 'email' && command !== 'phone' && command !== 'agent' && command !== 'fund' && command !== 'deposit' && command !== 'memory' && command !== 'backup' && command !== 'contacts' && command !== 'notifications' && command !== 'git') {
129131
return {
130132
command: 'help',
131133
demoOptions: { port: 8017, dir: '', verbose: false, refresh: false },
@@ -139,6 +141,7 @@ function parseArgs(): {
139141
musicOptions: {},
140142
searchOptions: {},
141143
memoryOptions: {},
144+
gitOptions: {},
142145
contactsOptions: {},
143146
};
144147
}
@@ -307,6 +310,16 @@ function parseArgs(): {
307310
topk: getArgValue('--topk', '') ? parseInt(getArgValue('--topk', '')!, 10) : undefined,
308311
};
309312

313+
// Parse git options
314+
const gitOptions: GitOptions = {
315+
writable: process.argv.includes('--writable'),
316+
ttl: getArgValue('--ttl', '') ? parseInt(getArgValue('--ttl', '')!, 10) : undefined,
317+
defaultBranch: getArgValue('--default-branch', ''),
318+
visibility: getArgValue('--visibility', ''),
319+
limit: getArgValue('--limit', '-l') ? parseInt(getArgValue('--limit', '-l')!, 10) : undefined,
320+
cursor: getArgValue('--cursor', ''),
321+
};
322+
310323
// Parse contacts options
311324
const contactsOptions: ContactsOptionsLocal = {
312325
name: getArgValue('--name', ''),
@@ -329,11 +342,12 @@ function parseArgs(): {
329342
musicOptions,
330343
searchOptions,
331344
memoryOptions,
345+
gitOptions,
332346
contactsOptions,
333347
};
334348
}
335349

336-
const { command, subCommand, demoOptions, createOptions, loginOptions, emailOptions, phoneOptions, paasOptions, paasArgs, toolArgs, musicOptions, searchOptions, memoryOptions, contactsOptions } = parseArgs();
350+
const { command, subCommand, demoOptions, createOptions, loginOptions, emailOptions, phoneOptions, paasOptions, paasArgs, toolArgs, musicOptions, searchOptions, memoryOptions, gitOptions, contactsOptions } = parseArgs();
337351

338352
// Extract positional args from argv, skipping flag values (e.g., --path <val> --topk <val>)
339353
function extractPositionalArgs(startIndex: number): string {
@@ -463,6 +477,10 @@ async function main() {
463477
await contactsCommand(subCommand || '', contactsOptions, process.argv[4]);
464478
break;
465479

480+
case 'git':
481+
await gitCommand(subCommand || '', gitOptions, process.argv[4]);
482+
break;
483+
466484
case 'notifications':
467485
await notificationsCommand(subCommand || '');
468486
break;

skills/atxp-git/SKILL.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
name: atxp-git
3+
description: Git repository hosting for ATXP-authenticated agents — create repos, get authenticated clone/push URLs, and manage repositories on code.storage
4+
compatibility: Requires Node.js >=18 and npx. Requires git for clone/push operations.
5+
tags: [git, repository, hosting, code-storage, version-control, source-code, agent-code]
6+
permissions:
7+
- network: "git.mcp.atxp.ai (HTTPS only), *.atxp.code.storage (Git over HTTPS)"
8+
- filesystem: "Cloned repositories are written to the local filesystem by git"
9+
- credentials: "ATXP_CONNECTION (required for all operations)"
10+
metadata:
11+
homepage: https://docs.atxp.ai
12+
source: https://github.com/atxp-dev/cli
13+
npm: https://www.npmjs.com/package/atxp
14+
requires:
15+
binaries: [node, npx, git]
16+
node: ">=18"
17+
env:
18+
- name: ATXP_CONNECTION
19+
description: Authentication token for the ATXP API. Required for all git operations.
20+
required: true
21+
---
22+
23+
# ATXP Git — Agent Repository Hosting
24+
25+
ATXP Git gives each agent a private namespace for Git repositories on [code.storage](https://code.storage). Agents can create repos, get authenticated clone/push URLs, and interact with them using standard Git commands. The MCP server handles provisioning and access control — all file operations happen through native Git.
26+
27+
## Ephemeral Remote URLs
28+
29+
**This is the most important concept to understand when using this tool.**
30+
31+
Remote URLs returned by `remote-url` contain a **time-limited JWT** embedded directly in the URL. This means:
32+
33+
1. **URLs expire.** The default TTL is 1 hour (3600 seconds). After expiry, any `git clone`, `git push`, `git pull`, or `git fetch` using that URL will fail with an authentication error.
34+
2. **URLs are not persistent.** Do **not** store remote URLs in config files, environment variables, or long-lived scripts expecting them to work indefinitely. They are single-use credentials with a short lifespan.
35+
3. **Refresh when expired.** When a git operation fails with an auth error, get a fresh URL and update the remote:
36+
37+
```bash
38+
# Get a new writable URL
39+
npx atxp@latest git remote-url my-project --writable
40+
41+
# Update the existing remote with the new URL
42+
git remote set-url origin <new-url>
43+
44+
# Retry the operation
45+
git push
46+
```
47+
48+
4. **No separate credential setup needed.** The URL embeds authentication — no `git credential` helper or SSH key configuration is required.
49+
5. **Read vs. write URLs.** Read-only URLs are free. Writable URLs cost $0.01 (this meters storage growth from pushes). Always request read-only unless you need to push.
50+
51+
### Practical Workflow for Long-Running Tasks
52+
53+
If your task involves multiple git operations over time:
54+
55+
```
56+
1. Get a writable URL at the start of your work session
57+
2. Clone and make changes
58+
3. Commit and push before the URL expires (within 1 hour)
59+
4. If you need to push again later, get a fresh URL first
60+
```
61+
62+
For tasks that may span multiple hours, request a new URL before each push rather than at the start. The `--ttl` flag can extend expiry up to the server maximum, but planning for refresh is more robust.
63+
64+
## Security Model
65+
66+
- **Repos are private by default** — only the owner can access them.
67+
- **Public repos** allow read-only access from other authenticated users via `remote-url` with `writable: false`.
68+
- **Write access** requires repo ownership. No collaborator model — repos are single-owner.
69+
- **URLs contain credentials** — treat `remote-url` output like a secret. Do not log, echo, or share writable URLs.
70+
- **Soft-delete** — deleted repos become immediately inaccessible but are permanently removed after 30 days.
71+
72+
## Commands Reference
73+
74+
| Command | Cost | Description |
75+
|---------|------|-------------|
76+
| `npx atxp@latest git create <repoName>` | $0.50 | Create a new repository |
77+
| `npx atxp@latest git list` | Free | List your repositories |
78+
| `npx atxp@latest git remote-url <repoName>` | Free | Get a read-only authenticated URL |
79+
| `npx atxp@latest git remote-url <repoName> --writable` | $0.01 | Get a writable authenticated URL |
80+
| `npx atxp@latest git delete <repoName>` | Free | Soft-delete a repository |
81+
| `npx atxp@latest git help` | Free | Show help |
82+
83+
### Options
84+
85+
| Option | Applies To | Description |
86+
|--------|-----------|-------------|
87+
| `--visibility <private\|public>` | create | Repository visibility (default: private) |
88+
| `--default-branch <name>` | create | Default branch name (default: main) |
89+
| `--writable` | remote-url | Request a push-capable URL ($0.01 instead of free) |
90+
| `--ttl <seconds>` | remote-url | URL expiry in seconds (default: 3600) |
91+
| `--limit <n>` | list | Max repos to return (default: 20, max: 100) |
92+
| `--cursor <token>` | list | Pagination cursor from a previous response |
93+
94+
### Repo Naming
95+
96+
Repository names must be lowercase alphanumeric with hyphens and underscores only (e.g., `my-project`, `agent_workspace`).
97+
98+
## Typical Agent Workflow
99+
100+
```bash
101+
# 1. Create a repository ($0.50)
102+
npx atxp@latest git create my-app
103+
104+
# 2. Get a writable URL ($0.01)
105+
npx atxp@latest git remote-url my-app --writable
106+
# Returns: https://t:eyJ...@atxp.code.storage/userid/my-app.git
107+
108+
# 3. Clone, work, push (standard git)
109+
git clone <url>
110+
cd my-app
111+
# ... make changes ...
112+
git add . && git commit -m "initial commit" && git push
113+
114+
# 4. Later — URL expired? Get a fresh one
115+
npx atxp@latest git remote-url my-app --writable
116+
git remote set-url origin <new-url>
117+
git push
118+
```
119+
120+
## Error Responses
121+
122+
| Scenario | Error message |
123+
|----------|---------------|
124+
| Not authenticated | `"ATXP authentication required"` |
125+
| Repo doesn't exist (or private + not owner) | `"Repository not found"` |
126+
| Write to another user's repo | `"Permission denied"` |
127+
| Repo name already taken | `"Repository already exists"` |
128+
| Service unavailable | `"Service temporarily unavailable. Please retry in a few seconds."` |
129+
| Git ref conflict | `"Conflict: ... Re-read the current state and retry."` |
130+
131+
## Visibility
132+
133+
| Mode | Owner | Other authenticated users |
134+
|------|-------|---------------------------|
135+
| **Private** | Full read/write | No access |
136+
| **Public** | Full read/write | Read-only (via `remote-url` with read-only default) |
137+
138+
## Rate Limits
139+
140+
- **Per-client:** 120 requests/minute
141+
- **Per-IP:** 10,000 requests/minute
142+
143+
Exceeding limits returns HTTP 429 with a `Retry-After` hint.
144+

skills/atxp/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ For programmatic access, ATXP exposes MCP-compatible tool servers:
394394
| `x-live-search.mcp.atxp.ai` | `x_live_search` |
395395
| `email.mcp.atxp.ai` | `email_check_inbox`, `email_get_message`, `email_send_email`, `email_reply`, `email_search`, `email_delete`, `email_get_attachment`, `email_claim_username`, `email_release_username` |
396396
| `phone.mcp.atxp.ai` | `phone_register`, `phone_release`, `phone_configure_voice`, `phone_send_sms`, `phone_check_sms`, `phone_get_sms`, `phone_get_attachment`, `phone_call`, `phone_check_calls`, `phone_get_call`, `phone_search` |
397+
| `git.mcp.atxp.ai` | `git_create_repo`, `git_list_repos`, `git_get_remote_url`, `git_delete_repo` (see `atxp-git` skill) |
397398
| `paas.mcp.atxp.ai` | PaaS tools (see `atxp-paas` skill) |
398399

399400
### TypeScript SDK
@@ -446,6 +447,14 @@ The **atxp-memory** skill provides agent memory management — cloud backup/rest
446447
npx skills add atxp-dev/cli --skill atxp-memory
447448
```
448449

450+
### ATXP Git
451+
452+
The **atxp-git** skill provides Git repository hosting for agents — create repos, get authenticated clone/push URLs, and manage repositories on [code.storage](https://code.storage). It is packaged as a separate skill because it interacts with a **different service** (`git.mcp.atxp.ai` and `*.atxp.code.storage`) and writes cloned repositories to the local filesystem via native Git. Remote URLs are **ephemeral** — they embed a time-limited JWT and expire after 1 hour by default. If your agent needs to host, clone, or push code, install it separately:
453+
454+
```bash
455+
npx skills add atxp-dev/cli --skill atxp-git
456+
```
457+
449458
## Support
450459

451460
```bash

0 commit comments

Comments
 (0)