@agenttower/cli 0.1.0
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 +36 -0
- package/bin/atc.js +3 -0
- package/dist/client.js +125 -0
- package/dist/index.js +190 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @agenttower/cli — `atc`
|
|
2
|
+
|
|
3
|
+
The Agent Tower CLI. Coordinate multiple coding agents working one repo: claim
|
|
4
|
+
scope before editing, get inline conflict verdicts, and share a decision log.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i -g @agenttower/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configure
|
|
13
|
+
|
|
14
|
+
Get a **frequency token** from the dashboard (https://app.agenttower.dev → your
|
|
15
|
+
project → Mint token), then:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export ATC_TOKEN=atcf_… # required
|
|
19
|
+
# export ATC_API=… # optional; defaults to the hosted prod API
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
atc checkin --task "refactor auth" # contact the Tower, get your callsign + a brief
|
|
26
|
+
atc brief # roster + claims + conflicts touching you + NOTAMs
|
|
27
|
+
atc claim "src/auth/**" # request scope; exit code 3 if it conflicts
|
|
28
|
+
atc squawk "rewriting User model" # status + heartbeat
|
|
29
|
+
atc note "auth uses JWT, 15min TTL" # write to the decision log (--pin to pin)
|
|
30
|
+
atc clear ["src/auth/**"] # release scope (all, or one glob)
|
|
31
|
+
atc checkout # leave; releases your claims
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Session state lives in `.atc/` in the worktree (gitignore it). Add `--json` to any
|
|
35
|
+
command for machine-readable output (useful in hooks). See the project docs for the
|
|
36
|
+
full protocol.
|
package/bin/atc.js
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadSession = exports.saveSession = exports.DEFAULT_API = void 0;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
exports.workspaceId = workspaceId;
|
|
39
|
+
exports.clearSession = clearSession;
|
|
40
|
+
exports.api = api;
|
|
41
|
+
exports.currentBranch = currentBranch;
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const node_crypto_1 = require("node:crypto");
|
|
45
|
+
/**
|
|
46
|
+
* Thin client for the Agent Tower /v1 API + local session state.
|
|
47
|
+
* Config is env-only (ATC_API / ATC_TOKEN). `.atc/` holds:
|
|
48
|
+
* workspace.json { workspaceId } — persistent takeover identity (ADR-0003)
|
|
49
|
+
* session.json { callsign, sessionToken, api } — lease-bound; cleared on checkout
|
|
50
|
+
* Both live in the worktree and must be gitignored.
|
|
51
|
+
*/
|
|
52
|
+
const ATC_DIR = path.join(process.cwd(), '.atc');
|
|
53
|
+
const WORKSPACE_FILE = path.join(ATC_DIR, 'workspace.json');
|
|
54
|
+
const SESSION_FILE = path.join(ATC_DIR, 'session.json');
|
|
55
|
+
/** Prod Tower API. Override with ATC_API (e.g. the dev stack). */
|
|
56
|
+
exports.DEFAULT_API = 'https://mz7tt5ee71.execute-api.us-east-1.amazonaws.com';
|
|
57
|
+
function loadConfig() {
|
|
58
|
+
return {
|
|
59
|
+
api: (process.env.ATC_API || exports.DEFAULT_API).replace(/\/$/, ''),
|
|
60
|
+
token: process.env.ATC_TOKEN ?? '',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function readJson(file) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function writeJson(file, data) {
|
|
72
|
+
fs.mkdirSync(ATC_DIR, { recursive: true });
|
|
73
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
74
|
+
}
|
|
75
|
+
/** Stable per-checkout workspace id (created once, survives checkout). */
|
|
76
|
+
function workspaceId() {
|
|
77
|
+
const existing = readJson(WORKSPACE_FILE);
|
|
78
|
+
if (existing?.workspaceId)
|
|
79
|
+
return existing.workspaceId;
|
|
80
|
+
const id = `ws_${(0, node_crypto_1.randomUUID)()}`;
|
|
81
|
+
writeJson(WORKSPACE_FILE, { workspaceId: id });
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
84
|
+
const saveSession = (s) => writeJson(SESSION_FILE, s);
|
|
85
|
+
exports.saveSession = saveSession;
|
|
86
|
+
const loadSession = () => readJson(SESSION_FILE);
|
|
87
|
+
exports.loadSession = loadSession;
|
|
88
|
+
function clearSession() {
|
|
89
|
+
try {
|
|
90
|
+
fs.rmSync(SESSION_FILE);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* already gone */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function api(cfg, bearer, method, apiPath, body) {
|
|
97
|
+
const res = await fetch(`${cfg.api}${apiPath}`, {
|
|
98
|
+
method,
|
|
99
|
+
headers: {
|
|
100
|
+
authorization: `Bearer ${bearer}`,
|
|
101
|
+
'content-type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
104
|
+
});
|
|
105
|
+
const text = await res.text();
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = text ? JSON.parse(text) : {};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
parsed = { error: text };
|
|
112
|
+
}
|
|
113
|
+
return { status: res.status, ok: res.ok, body: parsed };
|
|
114
|
+
}
|
|
115
|
+
/** Best-effort current git branch for display. */
|
|
116
|
+
function currentBranch() {
|
|
117
|
+
try {
|
|
118
|
+
const head = fs.readFileSync(path.join(process.cwd(), '.git', 'HEAD'), 'utf8').trim();
|
|
119
|
+
const m = /ref:\s+refs\/heads\/(.+)/.exec(head);
|
|
120
|
+
return m ? m[1] : undefined;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* atc — the Agent Tower CLI. Thin client of the hosted /v1 API.
|
|
5
|
+
* Config: ATC_API + ATC_TOKEN (frequency token from the dashboard). See
|
|
6
|
+
* docs/AGENT-INTERFACE.md. Exit codes: 0 ok · 1 usage/error · 2 unexpected · 3 conflict.
|
|
7
|
+
*/
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
const COMMANDS = ['checkin', 'brief', 'squawk', 'claim', 'clear', 'note', 'notes', 'checkout', 'config'];
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const positionals = [];
|
|
13
|
+
const flags = {};
|
|
14
|
+
const tags = [];
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a.startsWith('--')) {
|
|
18
|
+
const key = a.slice(2);
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
const boolFlags = ['pin', 'json'];
|
|
21
|
+
if (boolFlags.includes(key)) {
|
|
22
|
+
flags[key] = true;
|
|
23
|
+
}
|
|
24
|
+
else if (key === 'tag') {
|
|
25
|
+
if (next) {
|
|
26
|
+
tags.push(next);
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (next && !next.startsWith('--')) {
|
|
31
|
+
flags[key] = next;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
flags[key] = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
positionals.push(a);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { positionals, flags, tags };
|
|
43
|
+
}
|
|
44
|
+
function out(json, human, data) {
|
|
45
|
+
process.stdout.write(json ? JSON.stringify(data, null, 2) + '\n' : human + '\n');
|
|
46
|
+
}
|
|
47
|
+
function fail(msg, code = 1) {
|
|
48
|
+
process.stderr.write(`atc: ${msg}\n`);
|
|
49
|
+
process.exit(code);
|
|
50
|
+
}
|
|
51
|
+
function requireSession() {
|
|
52
|
+
const s = (0, client_1.loadSession)();
|
|
53
|
+
if (!s)
|
|
54
|
+
fail("not checked in — run 'atc checkin' first");
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
function renderConflicts(conflicts) {
|
|
58
|
+
if (!conflicts || conflicts.length === 0)
|
|
59
|
+
return '✓ no conflicts';
|
|
60
|
+
return (`⚠ CONFLICT (${conflicts.length}):\n` +
|
|
61
|
+
conflicts
|
|
62
|
+
.map((c) => ` ${c.by} holds ${c.theirGlob} (${c.status}, seen ${c.lastSeen}) vs your ${c.glob}`)
|
|
63
|
+
.join('\n'));
|
|
64
|
+
}
|
|
65
|
+
function printHelp() {
|
|
66
|
+
process.stdout.write(`atc ${VERSION} — Agent Tower CLI\n\n` +
|
|
67
|
+
`Usage: atc <command> [args]\n\n` +
|
|
68
|
+
` checkin [--task "..."] [--as <callsign>] [--cli <name>]\n` +
|
|
69
|
+
` brief\n` +
|
|
70
|
+
` squawk "<status>" [--code working|blocked|review|mayday]\n` +
|
|
71
|
+
` claim "<glob>" (exit 3 if it conflicts)\n` +
|
|
72
|
+
` clear ["<glob>"] (omit glob to release all)\n` +
|
|
73
|
+
` note "<body>" [--tag <t>]... [--pin] [--supersede <id>]\n` +
|
|
74
|
+
` notes [--tag <t>] list the decision log\n` +
|
|
75
|
+
` checkout\n` +
|
|
76
|
+
` config\n\n` +
|
|
77
|
+
`Env: ATC_API, ATC_TOKEN. Add --json for raw output.\n`);
|
|
78
|
+
}
|
|
79
|
+
async function main(argv) {
|
|
80
|
+
const [cmd, ...rest] = argv;
|
|
81
|
+
if (!cmd || cmd === '--help' || cmd === '-h')
|
|
82
|
+
return printHelp();
|
|
83
|
+
if (cmd === '--version' || cmd === '-v')
|
|
84
|
+
return void process.stdout.write(VERSION + '\n');
|
|
85
|
+
if (!COMMANDS.includes(cmd))
|
|
86
|
+
fail(`unknown command '${cmd}'. Run 'atc --help'.`);
|
|
87
|
+
const { positionals, flags, tags } = parseArgs(rest);
|
|
88
|
+
const json = flags.json === true;
|
|
89
|
+
const cfg = (0, client_1.loadConfig)();
|
|
90
|
+
if (cmd === 'config') {
|
|
91
|
+
return out(json, `api: ${cfg.api || '(unset)'}\ntoken: ${cfg.token ? '(set)' : '(unset)'}`, {
|
|
92
|
+
api: cfg.api || null,
|
|
93
|
+
token: cfg.token ? 'set' : null,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!cfg.api || !cfg.token) {
|
|
97
|
+
fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
|
|
98
|
+
}
|
|
99
|
+
if (cmd === 'checkin') {
|
|
100
|
+
const r = await (0, client_1.api)(cfg, cfg.token, 'POST', '/v1/sessions', {
|
|
101
|
+
workspaceId: (0, client_1.workspaceId)(),
|
|
102
|
+
callsign: flags.as,
|
|
103
|
+
cli: flags.cli || process.env.ATC_CLI || 'cli',
|
|
104
|
+
worktree: process.cwd(),
|
|
105
|
+
branch: (0, client_1.currentBranch)(),
|
|
106
|
+
task: flags.task,
|
|
107
|
+
});
|
|
108
|
+
if (!r.ok)
|
|
109
|
+
fail(r.body?.error ?? `checkin failed (${r.status})`, 2);
|
|
110
|
+
(0, client_1.saveSession)({ callsign: r.body.callsign, sessionToken: r.body.sessionToken, api: cfg.api });
|
|
111
|
+
const c = r.body.brief?.counts ?? {};
|
|
112
|
+
return out(json, `checked in as ${r.body.callsign} · ${c.sessions ?? 0} on board, ${c.claims ?? 0} claims, ${c.notams ?? 0} notams`, r.body);
|
|
113
|
+
}
|
|
114
|
+
const session = requireSession();
|
|
115
|
+
const tok = session.sessionToken;
|
|
116
|
+
switch (cmd) {
|
|
117
|
+
case 'brief': {
|
|
118
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', '/v1/brief');
|
|
119
|
+
if (!r.ok)
|
|
120
|
+
fail(r.body?.error ?? `brief failed (${r.status})`, 2);
|
|
121
|
+
const b = r.body;
|
|
122
|
+
const human = `roster: ${b.roster.map((x) => `${x.callsign}(${x.status})`).join(', ') || '(none)'}\n` +
|
|
123
|
+
`claims: ${b.claims.map((x) => `${x.callsign}:${x.glob}`).join(', ') || '(none)'}\n` +
|
|
124
|
+
renderConflicts(b.conflictsWithMine) +
|
|
125
|
+
`\nnotams: ${b.notams.length}/${b.counts.notams}`;
|
|
126
|
+
return out(json, human, b);
|
|
127
|
+
}
|
|
128
|
+
case 'squawk': {
|
|
129
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/squawk', {
|
|
130
|
+
status: positionals.join(' ') || undefined,
|
|
131
|
+
code: flags.code,
|
|
132
|
+
});
|
|
133
|
+
if (!r.ok)
|
|
134
|
+
fail(r.body?.error ?? `squawk failed (${r.status})`, 2);
|
|
135
|
+
return out(json, renderConflicts(r.body.conflictsWithMine), r.body);
|
|
136
|
+
}
|
|
137
|
+
case 'claim': {
|
|
138
|
+
const glob = positionals[0];
|
|
139
|
+
if (!glob)
|
|
140
|
+
fail('usage: atc claim "<glob>"');
|
|
141
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/claims', { glob });
|
|
142
|
+
if (!r.ok)
|
|
143
|
+
fail(r.body?.error ?? `claim failed (${r.status})`, 2);
|
|
144
|
+
const conflicts = r.body.conflicts ?? [];
|
|
145
|
+
out(json, `claimed ${glob}\n${renderConflicts(conflicts)}`, r.body);
|
|
146
|
+
if (conflicts.length > 0)
|
|
147
|
+
process.exit(3);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
case 'clear': {
|
|
151
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/clear', { glob: positionals[0] });
|
|
152
|
+
if (!r.ok)
|
|
153
|
+
fail(r.body?.error ?? `clear failed (${r.status})`, 2);
|
|
154
|
+
return out(json, `cleared: ${(r.body.cleared ?? []).join(', ') || '(none)'}`, r.body);
|
|
155
|
+
}
|
|
156
|
+
case 'note': {
|
|
157
|
+
const body = positionals.join(' ');
|
|
158
|
+
if (!body)
|
|
159
|
+
fail('usage: atc note "<body>" [--tag t] [--pin] [--supersede <id>]');
|
|
160
|
+
const supersedes = typeof flags.supersede === 'string' ? flags.supersede : undefined;
|
|
161
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/notams', { body, tags, pinned: flags.pin === true, supersedes });
|
|
162
|
+
if (!r.ok)
|
|
163
|
+
fail(r.body?.error ?? `note failed (${r.status})`, 2);
|
|
164
|
+
return out(json, `noted (${r.body.id})`, r.body);
|
|
165
|
+
}
|
|
166
|
+
case 'notes': {
|
|
167
|
+
const q = tags[0] ? `?tag=${encodeURIComponent(tags[0])}` : '';
|
|
168
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', `/v1/notams${q}`);
|
|
169
|
+
if (!r.ok)
|
|
170
|
+
fail(r.body?.error ?? `notes failed (${r.status})`, 2);
|
|
171
|
+
const human = (r.body.notams ?? [])
|
|
172
|
+
.map((n) => `${n.pinned ? '📌 ' : ''}[${n.id}] ${n.body}${n.tags?.length ? ' #' + n.tags.join(' #') : ''}`)
|
|
173
|
+
.join('\n') || '(no notams)';
|
|
174
|
+
return out(json, human, r.body);
|
|
175
|
+
}
|
|
176
|
+
case 'checkout': {
|
|
177
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/checkout');
|
|
178
|
+
(0, client_1.clearSession)();
|
|
179
|
+
if (!r.ok)
|
|
180
|
+
fail(r.body?.error ?? `checkout failed (${r.status})`, 2);
|
|
181
|
+
return out(json, 'checked out', r.body);
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
fail(`command '${cmd}' not wired`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
188
|
+
process.stderr.write(`atc: ${err?.message ?? err}\n`);
|
|
189
|
+
process.exit(2);
|
|
190
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agenttower/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "atc — the Agent Tower CLI. Coordinate multiple coding agents on one repo (claims, conflicts, decision log).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": { "type": "git", "url": "https://github.com/manateeit/agent-tower.git", "directory": "cli" },
|
|
7
|
+
"homepage": "https://app.agenttower.dev",
|
|
8
|
+
"keywords": ["agent-tower", "atc", "coding-agents", "coordination", "cli", "claude", "mcp"],
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"bin": { "atc": "bin/atc.js" },
|
|
11
|
+
"files": ["bin", "dist", "README.md"],
|
|
12
|
+
"engines": { "node": ">=18" },
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.16.10",
|
|
21
|
+
"typescript": "^5.6.2"
|
|
22
|
+
}
|
|
23
|
+
}
|