@agenttower/cli 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,9 @@
1
1
  # @agenttower/cli — `atc`
2
2
 
3
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.
4
+ scope before editing, get inline conflict verdicts, share a decision log, and
5
+ receive your **standing orders** at every checkin. Also the terminal for humans —
6
+ `atc login` manages projects, tokens, and standing orders without the dashboard.
5
7
 
6
8
  ## Install
7
9
 
@@ -9,28 +11,53 @@ scope before editing, get inline conflict verdicts, and share a decision log.
9
11
  npm i -g @agenttower/cli
10
12
  ```
11
13
 
12
- ## Configure
14
+ ## Wire a repo (fast path)
13
15
 
14
- Get a **frequency token** from the dashboard (https://app.agenttower.dev → your
15
- project Mint token), then:
16
+ ```bash
17
+ atc init # scaffolds .mcp.json + the AGENTS.md compliance block + .gitignore
18
+ ```
19
+
20
+ ## Agent plane (frequency token)
21
+
22
+ Get a **frequency token** (dashboard https://app.agenttower.dev → project →
23
+ Mint token, or `atc tokens mint` after login), then:
16
24
 
17
25
  ```bash
18
26
  export ATC_TOKEN=atcf_… # required
19
27
  # export ATC_API=… # optional; defaults to the hosted prod API
20
28
  ```
21
29
 
22
- ## Use
23
-
24
30
  ```bash
25
- atc checkin --task "refactor auth" # contact the Tower, get your callsign + a brief
26
- atc brief # roster + claims + conflicts touching you + NOTAMs
31
+ atc checkin --task "refactor auth" # contact the Tower; the brief arrives with
32
+ # your standing orders first follow them
33
+ atc standing # your assigned standing orders, in full
34
+ atc brief # roster + claims + conflicts + NOTAMs
27
35
  atc claim "src/auth/**" # request scope; exit code 3 if it conflicts
28
36
  atc squawk "rewriting User model" # status + heartbeat
29
- atc note "auth uses JWT, 15min TTL" # write to the decision log (--pin to pin)
37
+ atc note "auth uses JWT, 15min TTL" # decision log (--pin to pin; note show <id> for full)
38
+ atc standing propose handoff --body "…" # propose a DRAFT standing order (a human assigns)
30
39
  atc clear ["src/auth/**"] # release scope (all, or one glob)
31
40
  atc checkout # leave; releases your claims
32
41
  ```
33
42
 
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.
43
+ ## Human plane (`atc login`)
44
+
45
+ ```bash
46
+ atc login # opens the browser; approve; 30-day session
47
+ atc whoami / atc logout
48
+
49
+ atc projects [create "<name>"]
50
+ atc tokens --project <p> # list (shows each token's alignment)
51
+ atc tokens mint --project <p> --label colby --orders relay-agent
52
+ atc tokens align <tokenId> --orders a,b | --default
53
+ atc standing ls|show|create|edit|rm|assign --project <p>
54
+ ```
55
+
56
+ **Standing orders** are a per-project library of named instructions. Which orders
57
+ an agent receives is decided by its token's **alignment** (`mint --orders`,
58
+ realign anytime — it lands at the agent's next brief); unaligned tokens get the
59
+ project default set (`atc standing assign`).
60
+
61
+ Agent session state lives in `.atc/` in the worktree (gitignore it); your login
62
+ session lives in `~/.config/atc/`. Add `--json` to any command for
63
+ machine-readable output. Full protocol: https://app.agenttower.dev/docs
package/dist/client.js CHANGED
@@ -53,7 +53,7 @@ const ATC_DIR = path.join(process.cwd(), '.atc');
53
53
  const WORKSPACE_FILE = path.join(ATC_DIR, 'workspace.json');
54
54
  const SESSION_FILE = path.join(ATC_DIR, 'session.json');
55
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';
56
+ exports.DEFAULT_API = 'https://api.agenttower.dev';
57
57
  function loadConfig() {
58
58
  return {
59
59
  api: (process.env.ATC_API || exports.DEFAULT_API).replace(/\/$/, ''),
package/dist/human.js ADDED
@@ -0,0 +1,86 @@
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.loadHuman = loadHuman;
37
+ exports.saveHuman = saveHuman;
38
+ exports.clearHuman = clearHuman;
39
+ exports.humanApi = humanApi;
40
+ exports.requireHuman = requireHuman;
41
+ /**
42
+ * Human-plane session store for `atc login` / management commands.
43
+ * Stores the `atcu_` CLI session token at ~/.config/atc/session.json (chmod 0600).
44
+ * Deliberately separate from the agent session in .atc/session.json.
45
+ */
46
+ const fs = __importStar(require("node:fs"));
47
+ const os = __importStar(require("node:os"));
48
+ const path = __importStar(require("node:path"));
49
+ const client_1 = require("./client");
50
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'atc');
51
+ const SESSION_FILE = path.join(CONFIG_DIR, 'session.json');
52
+ function loadHuman() {
53
+ try {
54
+ const raw = fs.readFileSync(SESSION_FILE, 'utf8');
55
+ const s = JSON.parse(raw);
56
+ if (typeof s?.token === 'string' && s.token)
57
+ return s;
58
+ }
59
+ catch {
60
+ /* missing or unreadable */
61
+ }
62
+ return null;
63
+ }
64
+ function saveHuman(session) {
65
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
66
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
67
+ }
68
+ function clearHuman() {
69
+ try {
70
+ fs.rmSync(SESSION_FILE);
71
+ }
72
+ catch {
73
+ /* already gone */
74
+ }
75
+ }
76
+ /** Return an api() wrapper that uses the atcu_ token from the saved human session. */
77
+ function humanApi(session, method, apiPath, body) {
78
+ return (0, client_1.api)({ api: session.api, token: session.token }, session.token, method, apiPath, body);
79
+ }
80
+ /** Fail with a friendly message if no human session exists. */
81
+ function requireHuman(fail) {
82
+ const s = loadHuman();
83
+ if (!s)
84
+ fail("not logged in — run 'atc login'");
85
+ return s;
86
+ }
package/dist/index.js CHANGED
@@ -1,13 +1,54 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  /**
4
37
  * atc — the Agent Tower CLI. Thin client of the hosted /v1 API.
5
38
  * Config: ATC_API + ATC_TOKEN (frequency token from the dashboard). See
6
39
  * docs/AGENT-INTERFACE.md. Exit codes: 0 ok · 1 usage/error · 2 unexpected · 3 conflict.
7
40
  */
41
+ const fs = __importStar(require("node:fs"));
42
+ const child_process = __importStar(require("node:child_process"));
8
43
  const client_1 = require("./client");
9
- const VERSION = '0.1.0';
10
- const COMMANDS = ['checkin', 'brief', 'squawk', 'claim', 'clear', 'note', 'notes', 'checkout', 'config'];
44
+ const init_1 = require("./init");
45
+ const human_1 = require("./human");
46
+ const VERSION = '0.3.0';
47
+ const COMMANDS = [
48
+ 'init', 'checkin', 'brief', 'standing', 'squawk', 'claim', 'clear',
49
+ 'note', 'notes', 'checkout', 'config',
50
+ 'login', 'logout', 'whoami', 'projects', 'tokens',
51
+ ];
11
52
  function parseArgs(argv) {
12
53
  const positionals = [];
13
54
  const flags = {};
@@ -17,7 +58,7 @@ function parseArgs(argv) {
17
58
  if (a.startsWith('--')) {
18
59
  const key = a.slice(2);
19
60
  const next = argv[i + 1];
20
- const boolFlags = ['pin', 'json'];
61
+ const boolFlags = ['pin', 'json', 'yes', 'manual', 'draft', 'activate', 'default', 'none'];
21
62
  if (boolFlags.includes(key)) {
22
63
  flags[key] = true;
23
64
  }
@@ -65,16 +106,88 @@ function renderConflicts(conflicts) {
65
106
  function printHelp() {
66
107
  process.stdout.write(`atc ${VERSION} — Agent Tower CLI\n\n` +
67
108
  `Usage: atc <command> [args]\n\n` +
109
+ `Session commands (human login):\n` +
110
+ ` login [--api <url>] [--dashboard <url>] [--manual]\n` +
111
+ ` logout\n` +
112
+ ` whoami\n\n` +
113
+ `Management commands (require atc login):\n` +
114
+ ` projects [create "<name>"]\n` +
115
+ ` tokens --project <id|name> list tokens\n` +
116
+ ` tokens mint --project <p> [--label l] [--orders a,b,c]\n` +
117
+ ` tokens revoke <tokenId>\n` +
118
+ ` tokens align <tokenId> --orders a,b,c | --default\n` +
119
+ ` standing ls --project <p>\n` +
120
+ ` standing show <ref> --project <p>\n` +
121
+ ` standing create <name> --project <p> (--body "…" | --file f) [--draft]\n` +
122
+ ` standing edit <ref> --project <p> (--body "…" | --file f) [--name n] [--activate|--draft]\n` +
123
+ ` standing rm <ref> --project <p>\n` +
124
+ ` standing assign --project <p> <name1,name2,…> | --none\n` +
125
+ ` standing propose <name> (--body "…" | --file f) (agent plane, uses ATC_TOKEN)\n\n` +
126
+ `Agent commands (require ATC_API + ATC_TOKEN):\n` +
127
+ ` init [--token <freq-token>] [--yes] wire a project to Agent Tower\n` +
68
128
  ` checkin [--task "..."] [--as <callsign>] [--cli <name>]\n` +
69
129
  ` brief\n` +
130
+ ` standing print the frequency's standing orders in full\n` +
70
131
  ` squawk "<status>" [--code working|blocked|review|mayday]\n` +
71
132
  ` claim "<glob>" (exit 3 if it conflicts)\n` +
72
133
  ` clear ["<glob>"] (omit glob to release all)\n` +
73
134
  ` note "<body>" [--tag <t>]... [--pin] [--supersede <id>]\n` +
135
+ ` note show <id> full body of one NOTAM (briefs truncate)\n` +
74
136
  ` notes [--tag <t>] list the decision log\n` +
75
137
  ` checkout\n` +
76
138
  ` config\n\n` +
77
- `Env: ATC_API, ATC_TOKEN. Add --json for raw output.\n`);
139
+ `Env: ATC_API, ATC_TOKEN, ATC_DASHBOARD. Add --json for raw output.\n`);
140
+ }
141
+ /** Try to open a URL in the default browser (best-effort, never throws). */
142
+ function tryOpenBrowser(url) {
143
+ const cmds = {
144
+ darwin: ['open', [url]],
145
+ linux: ['xdg-open', [url]],
146
+ win32: ['cmd', ['/c', 'start', url]],
147
+ };
148
+ const entry = cmds[process.platform];
149
+ if (!entry)
150
+ return;
151
+ const [bin, args] = entry;
152
+ try {
153
+ child_process.spawn(bin, args, { detached: true, stdio: 'ignore' }).unref();
154
+ }
155
+ catch {
156
+ /* ignore */
157
+ }
158
+ }
159
+ /** Resolve a project ref (id or name) to an id via GET /v1/projects. */
160
+ async function resolveProject(session, ref) {
161
+ if (ref.startsWith('prj_'))
162
+ return ref;
163
+ const r = await (0, human_1.humanApi)(session, 'GET', '/v1/projects');
164
+ if (!r.ok)
165
+ fail(r.body?.error ?? `could not list projects (${r.status})`);
166
+ const projects = r.body.projects ?? [];
167
+ const byId = projects.find((p) => p.id === ref);
168
+ if (byId)
169
+ return byId.id;
170
+ const matches = projects.filter((p) => p.name.toLowerCase() === ref.toLowerCase());
171
+ if (matches.length === 1)
172
+ return matches[0].id;
173
+ if (matches.length === 0)
174
+ fail(`no project named "${ref}"`);
175
+ fail(`ambiguous project name "${ref}" — matches: ${matches.map((p) => `${p.id} (${p.name})`).join(', ')}`);
176
+ }
177
+ /** Read body text from --body "…" or --file f flags. */
178
+ function readBodyFlag(flags) {
179
+ if (typeof flags.body === 'string')
180
+ return flags.body;
181
+ if (typeof flags.file === 'string') {
182
+ try {
183
+ return fs.readFileSync(flags.file, 'utf8');
184
+ }
185
+ catch (err) {
186
+ const msg = err instanceof Error ? err.message : String(err);
187
+ fail(`could not read file "${flags.file}": ${msg}`);
188
+ }
189
+ }
190
+ return undefined;
78
191
  }
79
192
  async function main(argv) {
80
193
  const [cmd, ...rest] = argv;
@@ -87,12 +200,335 @@ async function main(argv) {
87
200
  const { positionals, flags, tags } = parseArgs(rest);
88
201
  const json = flags.json === true;
89
202
  const cfg = (0, client_1.loadConfig)();
203
+ if (cmd === 'init') {
204
+ return (0, init_1.runInit)({
205
+ token: typeof flags.token === 'string' ? flags.token : undefined,
206
+ yes: flags.yes === true,
207
+ json,
208
+ });
209
+ }
90
210
  if (cmd === 'config') {
91
211
  return out(json, `api: ${cfg.api || '(unset)'}\ntoken: ${cfg.token ? '(set)' : '(unset)'}`, {
92
212
  api: cfg.api || null,
93
213
  token: cfg.token ? 'set' : null,
94
214
  });
95
215
  }
216
+ // ---- Human-plane session commands ----
217
+ if (cmd === 'login') {
218
+ const apiBase = (typeof flags.api === 'string' ? flags.api : cfg.api || client_1.DEFAULT_API).replace(/\/$/, '');
219
+ const dashboardBase = (typeof flags.dashboard === 'string'
220
+ ? flags.dashboard
221
+ : process.env.ATC_DASHBOARD || 'https://app.agenttower.dev').replace(/\/$/, '');
222
+ const manual = flags.manual === true;
223
+ // Start the device flow
224
+ const startRes = await (0, client_1.api)({ api: apiBase, token: '' }, '', 'POST', '/v1/cli-auth/start').catch((err) => {
225
+ const msg = err instanceof Error ? err.message : String(err);
226
+ fail(`login failed: ${msg}`);
227
+ });
228
+ if (!startRes.ok) {
229
+ fail(startRes.body?.error ?? `login start failed (${startRes.status})`);
230
+ }
231
+ const { authId, pollSecret, expiresInSeconds } = startRes.body;
232
+ const verifyUrl = `${dashboardBase}/cli-auth?code=${authId}`;
233
+ process.stdout.write(`Open this URL to approve the login:\n ${verifyUrl}\n`);
234
+ if (!manual) {
235
+ tryOpenBrowser(verifyUrl);
236
+ }
237
+ // Poll until approved or expired
238
+ const deadline = Date.now() + expiresInSeconds * 1000;
239
+ let approved = false;
240
+ let token = '';
241
+ let orgId = '';
242
+ process.stderr.write('Waiting for browser approval');
243
+ const interval = setInterval(() => process.stderr.write('.'), 2000);
244
+ const cleanup = () => {
245
+ clearInterval(interval);
246
+ process.stderr.write('\n');
247
+ };
248
+ // Handle Ctrl-C gracefully
249
+ process.on('SIGINT', () => {
250
+ cleanup();
251
+ process.stderr.write('atc: login cancelled\n');
252
+ process.exit(1);
253
+ });
254
+ while (Date.now() < deadline && !approved) {
255
+ await new Promise((r) => setTimeout(r, 2000));
256
+ try {
257
+ const pollRes = await (0, client_1.api)({ api: apiBase, token: '' }, '', 'GET', `/v1/cli-auth/${encodeURIComponent(authId)}?secret=${encodeURIComponent(pollSecret)}`);
258
+ if (pollRes.ok && pollRes.body?.status === 'approved') {
259
+ approved = true;
260
+ token = pollRes.body.token;
261
+ orgId = pollRes.body.orgId;
262
+ }
263
+ }
264
+ catch {
265
+ /* transient network error — keep polling */
266
+ }
267
+ }
268
+ cleanup();
269
+ if (!approved) {
270
+ fail('login timed out — run atc login again');
271
+ }
272
+ // Fetch identity
273
+ let userId;
274
+ try {
275
+ const meRes = await (0, client_1.api)({ api: apiBase, token }, token, 'GET', '/v1/me');
276
+ if (meRes.ok) {
277
+ userId = meRes.body?.userId;
278
+ orgId = meRes.body?.orgId || orgId;
279
+ }
280
+ }
281
+ catch {
282
+ /* best-effort */
283
+ }
284
+ (0, human_1.saveHuman)({ api: apiBase, token, orgId, userId });
285
+ return out(json, `logged in as ${userId ?? '(unknown)'} (org ${orgId})`, { userId, orgId });
286
+ }
287
+ if (cmd === 'logout') {
288
+ const session = (0, human_1.loadHuman)();
289
+ if (session) {
290
+ try {
291
+ await (0, human_1.humanApi)(session, 'DELETE', '/v1/cli-auth/session');
292
+ }
293
+ catch {
294
+ /* best-effort */
295
+ }
296
+ }
297
+ (0, human_1.clearHuman)();
298
+ return out(json, 'logged out', { ok: true });
299
+ }
300
+ if (cmd === 'whoami') {
301
+ const session = (0, human_1.requireHuman)(fail);
302
+ const r = await (0, human_1.humanApi)(session, 'GET', '/v1/me');
303
+ if (!r.ok)
304
+ fail(r.body?.error ?? `whoami failed (${r.status})`);
305
+ return out(json, `userId: ${r.body.userId}\norgId: ${r.body.orgId}\nrole: ${r.body.orgRole ?? '(unknown)'}`, r.body);
306
+ }
307
+ if (cmd === 'projects') {
308
+ const session = (0, human_1.requireHuman)(fail);
309
+ const sub = positionals[0];
310
+ if (!sub || sub === 'list') {
311
+ const r = await (0, human_1.humanApi)(session, 'GET', '/v1/projects');
312
+ if (!r.ok)
313
+ fail(r.body?.error ?? `projects failed (${r.status})`);
314
+ const rows = (r.body.projects ?? []);
315
+ const human = rows.length
316
+ ? rows.map((p) => `${p.id} ${p.name} (${p.status})`).join('\n')
317
+ : '(no projects)';
318
+ return out(json, human, r.body);
319
+ }
320
+ if (sub === 'create') {
321
+ const name = positionals[1] ?? (typeof flags.name === 'string' ? flags.name : undefined);
322
+ if (!name)
323
+ fail('usage: atc projects create "<name>"');
324
+ const r = await (0, human_1.humanApi)(session, 'POST', '/v1/projects', { name });
325
+ if (!r.ok)
326
+ fail(r.body?.error ?? `projects create failed (${r.status})`);
327
+ return out(json, `created project ${r.body.id} — ${r.body.name}`, r.body);
328
+ }
329
+ fail(`unknown projects subcommand "${sub}"`);
330
+ }
331
+ if (cmd === 'tokens') {
332
+ const session = (0, human_1.requireHuman)(fail);
333
+ const sub = positionals[0];
334
+ if (!sub || sub === 'list') {
335
+ const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
336
+ if (!projectRef)
337
+ fail('usage: atc tokens --project <id|name>');
338
+ const pid = await resolveProject(session, projectRef);
339
+ const r = await (0, human_1.humanApi)(session, 'GET', `/v1/projects/${encodeURIComponent(pid)}/tokens`);
340
+ if (!r.ok)
341
+ fail(r.body?.error ?? `tokens list failed (${r.status})`);
342
+ const rows = (r.body.tokens ?? []);
343
+ const human = rows.length
344
+ ? rows.map((t) => `${t.tokenId} ${t.label ?? '(no label)'} ${t.revoked ? '[revoked]' : '[active]'} ${t.createdAt}`).join('\n')
345
+ : '(no tokens)';
346
+ return out(json, human, r.body);
347
+ }
348
+ if (sub === 'mint') {
349
+ const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
350
+ if (!projectRef)
351
+ fail('usage: atc tokens mint --project <p> [--label l] [--orders a,b,c]');
352
+ const pid = await resolveProject(session, projectRef);
353
+ const label = typeof flags.label === 'string' ? flags.label : undefined;
354
+ const ordersRaw = typeof flags.orders === 'string' ? flags.orders : undefined;
355
+ const standingOrderIds = ordersRaw ? ordersRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
356
+ const r = await (0, human_1.humanApi)(session, 'POST', `/v1/projects/${encodeURIComponent(pid)}/tokens`, {
357
+ label,
358
+ standingOrderIds,
359
+ });
360
+ if (!r.ok)
361
+ fail(r.body?.error ?? `tokens mint failed (${r.status})`);
362
+ const body = r.body;
363
+ // Token shown ONCE — warning on stderr so --json stdout stays parseable.
364
+ process.stderr.write(`WARNING: copy this token now — it will not be shown again.\n`);
365
+ return out(json, `token: ${body.token}\nid: ${body.tokenId}${body.label ? `\nlabel: ${body.label}` : ''}`, r.body);
366
+ }
367
+ if (sub === 'revoke') {
368
+ const tokenId = positionals[1];
369
+ if (!tokenId)
370
+ fail('usage: atc tokens revoke <tokenId>');
371
+ const r = await (0, human_1.humanApi)(session, 'DELETE', `/v1/tokens/${encodeURIComponent(tokenId)}`);
372
+ if (!r.ok)
373
+ fail(r.body?.error ?? `tokens revoke failed (${r.status})`);
374
+ return out(json, `revoked ${tokenId}`, r.body);
375
+ }
376
+ if (sub === 'align') {
377
+ const tokenId = positionals[1];
378
+ if (!tokenId)
379
+ fail('usage: atc tokens align <tokenId> --orders a,b,c | --default');
380
+ let orders;
381
+ if (flags.default === true) {
382
+ orders = null;
383
+ }
384
+ else {
385
+ const ordersRaw = typeof flags.orders === 'string' ? flags.orders : undefined;
386
+ if (!ordersRaw)
387
+ fail('usage: atc tokens align <tokenId> --orders a,b,c | --default');
388
+ orders = ordersRaw.split(',').map((s) => s.trim()).filter(Boolean);
389
+ }
390
+ const r = await (0, human_1.humanApi)(session, 'PUT', `/v1/tokens/${encodeURIComponent(tokenId)}/standing`, { orders });
391
+ if (!r.ok)
392
+ fail(r.body?.error ?? `tokens align failed (${r.status})`);
393
+ return out(json, orders === null ? `${tokenId} → project default` : `${tokenId} → [${orders.join(', ')}]`, r.body);
394
+ }
395
+ fail(`unknown tokens subcommand "${sub}"`);
396
+ }
397
+ // ---- standing subcommands (management + agent) ----
398
+ if (cmd === 'standing') {
399
+ // No sub → agent plane read (existing behaviour; requires ATC_TOKEN session)
400
+ if (!positionals[0]) {
401
+ if (!cfg.api || !cfg.token) {
402
+ fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
403
+ }
404
+ const session = requireSession();
405
+ const tok = session.sessionToken;
406
+ const r = await (0, client_1.api)(cfg, tok, 'GET', '/v1/standing');
407
+ if (!r.ok)
408
+ fail(r.body?.error ?? `standing failed (${r.status})`, 2);
409
+ const s = r.body.standing;
410
+ return out(json, s
411
+ ? `standing orders (${s.source}): ${s.orders.map((o) => `${o.name} v${o.version}`).join(', ')}\n\n${s.body}`
412
+ : '(no standing orders assigned to this session)', r.body);
413
+ }
414
+ const sub = positionals[0];
415
+ // propose — agent plane: uses the existing repo session token
416
+ if (sub === 'propose') {
417
+ if (!cfg.api || !cfg.token) {
418
+ fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
419
+ }
420
+ const session = requireSession();
421
+ const tok = session.sessionToken;
422
+ const name = positionals[1];
423
+ if (!name)
424
+ fail('usage: atc standing propose <name> (--body "…" | --file f)');
425
+ const body = readBodyFlag(flags);
426
+ if (!body)
427
+ fail('usage: atc standing propose <name> (--body "…" | --file f)');
428
+ const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/standing-orders', { name, body });
429
+ if (!r.ok)
430
+ fail(r.body?.error ?? `standing propose failed (${r.status})`, 2);
431
+ const order = r.body.order ?? r.body;
432
+ return out(json, `draft ${order.id ?? order.orderId} proposed — a human can activate/assign it`, r.body);
433
+ }
434
+ // Management subcommands below all require human login
435
+ const humanSession = (0, human_1.requireHuman)(fail);
436
+ const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
437
+ if (!projectRef && sub !== 'propose') {
438
+ fail('--project <id|name> is required for standing management commands');
439
+ }
440
+ const pid = projectRef ? await resolveProject(humanSession, projectRef) : '';
441
+ if (sub === 'ls') {
442
+ const r = await (0, human_1.humanApi)(humanSession, 'GET', `/v1/projects/${encodeURIComponent(pid)}/standing-orders`);
443
+ if (!r.ok)
444
+ fail(r.body?.error ?? `standing ls failed (${r.status})`);
445
+ const { orders, defaultAssignment } = r.body;
446
+ if (!orders.length)
447
+ return out(json, '(no standing orders)', r.body);
448
+ const human = orders
449
+ .map((o) => {
450
+ const marker = defaultAssignment.includes(o.id) ? ' [default]' : '';
451
+ return `${o.id} ${o.name} ${o.status} v${o.version} ${o.updatedAt}${marker}`;
452
+ })
453
+ .join('\n');
454
+ return out(json, human, r.body);
455
+ }
456
+ if (sub === 'show') {
457
+ const ref = positionals[1];
458
+ if (!ref)
459
+ fail('usage: atc standing show <ref> --project <p>');
460
+ const r = await (0, human_1.humanApi)(humanSession, 'GET', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`);
461
+ if (!r.ok)
462
+ fail(r.body?.error ?? `standing show failed (${r.status})`);
463
+ const { order, history } = r.body;
464
+ const histLine = history?.length
465
+ ? `\nhistory: ${history.map((h) => `v${h.version} (${h.updatedAt})`).join(', ')}`
466
+ : '';
467
+ return out(json, `[${order.id}] ${order.name} ${order.status} v${order.version} ${order.updatedAt}${histLine}\n\n${order.body}`, r.body);
468
+ }
469
+ if (sub === 'create') {
470
+ const name = positionals[1];
471
+ if (!name)
472
+ fail('usage: atc standing create <name> --project <p> (--body "…" | --file f) [--draft]');
473
+ const body = readBodyFlag(flags);
474
+ if (!body)
475
+ fail('--body "…" or --file <path> is required');
476
+ const status = flags.draft === true ? 'draft' : 'active';
477
+ const r = await (0, human_1.humanApi)(humanSession, 'POST', `/v1/projects/${encodeURIComponent(pid)}/standing-orders`, {
478
+ name,
479
+ body,
480
+ status,
481
+ });
482
+ if (!r.ok)
483
+ fail(r.body?.error ?? `standing create failed (${r.status})`);
484
+ const order = r.body.order;
485
+ return out(json, `created ${order.id} ${order.name} ${order.status}`, r.body);
486
+ }
487
+ if (sub === 'edit') {
488
+ const ref = positionals[1];
489
+ if (!ref)
490
+ fail('usage: atc standing edit <ref> --project <p> (--body "…" | --file f) [--name n] [--activate|--draft]');
491
+ const body = readBodyFlag(flags);
492
+ const name = typeof flags.name === 'string' ? flags.name : undefined;
493
+ const status = flags.activate === true ? 'active' : flags.draft === true ? 'draft' : undefined;
494
+ if (!body && !name && !status) {
495
+ fail('standing edit requires at least one of --body/--file, --name, --activate, or --draft');
496
+ }
497
+ const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`, { body, name, status });
498
+ if (!r.ok)
499
+ fail(r.body?.error ?? `standing edit failed (${r.status})`);
500
+ const order = r.body.order;
501
+ return out(json, `updated ${order.id} ${order.name} ${order.status} v${order.version}`, r.body);
502
+ }
503
+ if (sub === 'rm') {
504
+ const ref = positionals[1];
505
+ if (!ref)
506
+ fail('usage: atc standing rm <ref> --project <p>');
507
+ const r = await (0, human_1.humanApi)(humanSession, 'DELETE', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`);
508
+ if (!r.ok)
509
+ fail(r.body?.error ?? `standing rm failed (${r.status})`);
510
+ return out(json, `deleted ${r.body.deleted ?? ref}`, r.body);
511
+ }
512
+ if (sub === 'assign') {
513
+ if (flags.none === true) {
514
+ const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-assignment`, { orders: [] });
515
+ if (!r.ok)
516
+ fail(r.body?.error ?? `standing assign failed (${r.status})`);
517
+ return out(json, 'default assignment cleared', r.body);
518
+ }
519
+ // positionals[1] is the comma-separated list, or could be positionals[1..n]
520
+ const ordersRaw = positionals.slice(1).join(',');
521
+ if (!ordersRaw)
522
+ fail('usage: atc standing assign --project <p> <name1,name2,…> | --none');
523
+ const orders = ordersRaw.split(',').map((s) => s.trim()).filter(Boolean);
524
+ const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-assignment`, { orders });
525
+ if (!r.ok)
526
+ fail(r.body?.error ?? `standing assign failed (${r.status})`);
527
+ return out(json, `default assignment → [${(r.body.defaultAssignment ?? orders).join(', ')}]`, r.body);
528
+ }
529
+ fail(`unknown standing subcommand "${sub}"`);
530
+ }
531
+ // ---- Agent-plane commands: require ATC_API + ATC_TOKEN ----
96
532
  if (!cfg.api || !cfg.token) {
97
533
  fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
98
534
  }
@@ -109,7 +545,11 @@ async function main(argv) {
109
545
  fail(r.body?.error ?? `checkin failed (${r.status})`, 2);
110
546
  (0, client_1.saveSession)({ callsign: r.body.callsign, sessionToken: r.body.sessionToken, api: cfg.api });
111
547
  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);
548
+ const so = r.body.brief?.standing;
549
+ const standing = so
550
+ ? `\n⚑ standing orders present (${(so.orders ?? []).map((o) => o.name).join(', ') || 'assigned'}) — read them: atc standing`
551
+ : '';
552
+ return out(json, `checked in as ${r.body.callsign} · ${c.sessions ?? 0} on board, ${c.claims ?? 0} claims, ${c.notams ?? 0} notams${standing}`, r.body);
113
553
  }
114
554
  const session = requireSession();
115
555
  const tok = session.sessionToken;
@@ -119,10 +559,18 @@ async function main(argv) {
119
559
  if (!r.ok)
120
560
  fail(r.body?.error ?? `brief failed (${r.status})`, 2);
121
561
  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` +
562
+ const standingLine = b.standing
563
+ ? `standing orders (${b.standing.source}): ${b.standing.orders.map((o) => o.name).join(', ')}` +
564
+ `${b.standing.truncated ? ' (truncated here — atc standing for full)' : ''}\n${b.standing.body}\n---\n`
565
+ : '';
566
+ const who = (x) => `${x.callsign}(${x.code ?? x.statusText ?? x.status ?? 'working'}${x.claimCount ? `, ${x.claimCount} claims` : ''})`;
567
+ const human = standingLine +
568
+ `roster: ${b.roster.map(who).join(', ') || '(none)'}\n` +
569
+ `claims: ${b.claims.map((x) => `${x.callsign}:${x.glob}`).join(', ') || '(none)'}` +
570
+ (b.counts.claims > b.claims.length ? ` (+${b.counts.claims - b.claims.length} more on board)` : '') +
571
+ `\n` +
124
572
  renderConflicts(b.conflictsWithMine) +
125
- `\nnotams: ${b.notams.length}/${b.counts.notams}`;
573
+ `\nnotams: ${b.notams.length} shown (pinned + recent; truncated bodies — 'atc note show <id>' for full)`;
126
574
  return out(json, human, b);
127
575
  }
128
576
  case 'squawk': {
@@ -154,9 +602,20 @@ async function main(argv) {
154
602
  return out(json, `cleared: ${(r.body.cleared ?? []).join(', ') || '(none)'}`, r.body);
155
603
  }
156
604
  case 'note': {
605
+ if (positionals[0] === 'show') {
606
+ const id = positionals[1];
607
+ if (!id)
608
+ fail('usage: atc note show <id>');
609
+ const r = await (0, client_1.api)(cfg, tok, 'GET', `/v1/notams/${encodeURIComponent(id)}`);
610
+ if (!r.ok)
611
+ fail(r.body?.error ?? `note show failed (${r.status})`, 2);
612
+ const n = r.body.notam;
613
+ return out(json, `${n.pinned ? '📌 ' : ''}[${n.id}] by ${n.author} at ${n.createdAt}` +
614
+ `${n.tags?.length ? ' #' + n.tags.join(' #') : ''}${n.superseded ? ' (superseded)' : ''}\n${n.body}`, r.body);
615
+ }
157
616
  const body = positionals.join(' ');
158
617
  if (!body)
159
- fail('usage: atc note "<body>" [--tag t] [--pin] [--supersede <id>]');
618
+ fail('usage: atc note "<body>" [--tag t] [--pin] [--supersede <id>] | atc note show <id>');
160
619
  const supersedes = typeof flags.supersede === 'string' ? flags.supersede : undefined;
161
620
  const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/notams', { body, tags, pinned: flags.pin === true, supersedes });
162
621
  if (!r.ok)
package/dist/init.js ADDED
@@ -0,0 +1,393 @@
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.runInit = runInit;
37
+ /**
38
+ * atc init — wire a project to Agent Tower in one command.
39
+ *
40
+ * Steps (idempotent, fail-open per step):
41
+ * 1. Plan + confirm (unless --yes)
42
+ * 2. .mcp.json — create or merge agent-tower entry
43
+ * 3. AGENTS.md — append compliance block if absent
44
+ * 4. .gitignore — ensure .atc/ line exists
45
+ * 5. Checkin + brief if a frequency token is available
46
+ */
47
+ const fs = __importStar(require("node:fs"));
48
+ const path = __importStar(require("node:path"));
49
+ const readline = __importStar(require("node:readline"));
50
+ const client_1 = require("./client");
51
+ // ---------------------------------------------------------------------------
52
+ // Embedded compliance block (adapted from templates/agent-tower.md).
53
+ // The CLI ships standalone via npm and cannot read repo files at runtime.
54
+ // ---------------------------------------------------------------------------
55
+ const COMPLIANCE_BLOCK = `
56
+ ## Agent Tower coordination
57
+
58
+ You are one of several agents working this repository. Coordinate through the
59
+ **Agent Tower** so you don't collide with other agents' edits, duplicate work, or
60
+ re-decide settled questions. Prefer the \`atc_*\` MCP tools; if they're unavailable,
61
+ use the \`atc\` CLI (same actions).
62
+
63
+ **At the start of a session:**
64
+ - \`atc_checkin\` (state your task) — establishes your callsign.
65
+ - \`atc_brief\` — read the roster, active claims, conflicts touching you, and NOTAMs
66
+ (the decision log) so you catch up on what's already been decided.
67
+ - **Brief and checkin return this frequency's standing orders first — read and follow
68
+ them.** For the full text: \`atc standing\` / \`atc_standing\`.
69
+
70
+ **Before editing any area:**
71
+ - \`atc_claim "<glob>"\` (e.g. \`src/auth/**\`) **before** you start editing it.
72
+ - If the response lists **conflicts**, another agent holds overlapping scope —
73
+ **do not edit.** \`atc_squawk\` with code \`blocked\` and surface it to the human.
74
+ If it can't be resolved, \`atc_squawk\` code \`mayday\` and stop.
75
+
76
+ **As you work:**
77
+ - \`atc_squawk "<what you're doing>"\` at checkpoints — it's also your heartbeat and
78
+ shows conflicts that have appeared on your claims.
79
+
80
+ **When you settle a cross-cutting decision** (a contract, convention, TTL, etc.):
81
+ - \`atc_note "<the decision>"\` so the next agent reads it in their brief. Pin
82
+ durable contracts.
83
+
84
+ **When you finish / end the session:**
85
+ - \`atc_clear\` to release scope you no longer need; \`atc_checkout\` on session end.
86
+
87
+ Claims are **advisory** — the Tower detects and surfaces conflicts; it never blocks
88
+ your edits. Respect them anyway: that's the whole point.
89
+
90
+ <!-- CLI equivalents: atc checkin --task "…" · atc brief · atc claim "<glob>" ·
91
+ atc squawk "…" [--code blocked|mayday] · atc note "…" [--pin] · atc clear · atc checkout
92
+ Standing orders: atc standing -->
93
+ `.trimStart();
94
+ const COMPLIANCE_MARKER = '## Agent Tower coordination';
95
+ // ---------------------------------------------------------------------------
96
+ // MCP entry we always want present
97
+ // ---------------------------------------------------------------------------
98
+ const MCP_SERVER_KEY = 'agent-tower';
99
+ const MCP_SERVER_ENTRY = {
100
+ command: 'npx',
101
+ args: ['-y', '@agenttower/mcp'],
102
+ env: {
103
+ ATC_API: '${ATC_API}',
104
+ ATC_TOKEN: '${ATC_TOKEN}',
105
+ },
106
+ };
107
+ // ---------------------------------------------------------------------------
108
+ // Helpers
109
+ // ---------------------------------------------------------------------------
110
+ function readText(file) {
111
+ try {
112
+ return fs.readFileSync(file, 'utf8');
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ function readJson(file) {
119
+ const text = readText(file);
120
+ if (!text)
121
+ return null;
122
+ try {
123
+ return JSON.parse(text);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /** Deep-equal check limited to what we need for the MCP entry. */
130
+ function mcpEntryMatches(existing) {
131
+ if (!existing || typeof existing !== 'object')
132
+ return false;
133
+ const e = existing;
134
+ if (e.command !== 'npx')
135
+ return false;
136
+ const args = e.args;
137
+ if (!Array.isArray(args))
138
+ return false;
139
+ return args.includes('@agenttower/mcp');
140
+ }
141
+ async function confirm(question) {
142
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
+ return new Promise((resolve) => {
144
+ rl.question(question, (answer) => {
145
+ rl.close();
146
+ resolve(answer.trim().toLowerCase() === 'y');
147
+ });
148
+ });
149
+ }
150
+ function planMcp(cwd) {
151
+ const file = path.join(cwd, '.mcp.json');
152
+ const existing = readJson(file);
153
+ if (!existing) {
154
+ return {
155
+ action: 'created',
156
+ preview: `+ mcpServers["${MCP_SERVER_KEY}"] = { command: "npx", args: ["-y", "@agenttower/mcp"], env: {...} }`,
157
+ };
158
+ }
159
+ const servers = existing.mcpServers;
160
+ if (servers && mcpEntryMatches(servers[MCP_SERVER_KEY])) {
161
+ return { action: 'unchanged' };
162
+ }
163
+ return {
164
+ action: 'updated',
165
+ preview: `+ mcpServers["${MCP_SERVER_KEY}"] = { command: "npx", args: ["-y", "@agenttower/mcp"], env: {...} }`,
166
+ };
167
+ }
168
+ function applyMcp(cwd) {
169
+ const file = path.join(cwd, '.mcp.json');
170
+ const existing = readJson(file) ?? {};
171
+ const servers = existing.mcpServers ?? {};
172
+ servers[MCP_SERVER_KEY] = MCP_SERVER_ENTRY;
173
+ const updated = { ...existing, mcpServers: servers };
174
+ fs.writeFileSync(file, JSON.stringify(updated, null, 2) + '\n');
175
+ }
176
+ function planAgentsMd(cwd) {
177
+ const file = path.join(cwd, 'AGENTS.md');
178
+ const existing = readText(file);
179
+ if (existing && existing.includes(COMPLIANCE_MARKER)) {
180
+ return { action: 'unchanged' };
181
+ }
182
+ return {
183
+ action: existing ? 'updated' : 'created',
184
+ preview: `+ ## Agent Tower coordination\\n+ (compliance block, ~30 lines)`,
185
+ };
186
+ }
187
+ function applyAgentsMd(cwd) {
188
+ const file = path.join(cwd, 'AGENTS.md');
189
+ const existing = readText(file);
190
+ if (existing && existing.includes(COMPLIANCE_MARKER))
191
+ return; // idempotent guard
192
+ const separator = existing && !existing.endsWith('\n\n') ? '\n' : '';
193
+ const content = existing ? existing + separator + COMPLIANCE_BLOCK : COMPLIANCE_BLOCK;
194
+ fs.writeFileSync(file, content);
195
+ }
196
+ function planGitignore(cwd) {
197
+ const file = path.join(cwd, '.gitignore');
198
+ const existing = readText(file);
199
+ if (existing) {
200
+ // Match exact line .atc/ (with or without trailing newline variations)
201
+ const lines = existing.split('\n').map((l) => l.trim());
202
+ if (lines.includes('.atc/'))
203
+ return { action: 'unchanged' };
204
+ return { action: 'updated', preview: '+ .atc/' };
205
+ }
206
+ return { action: 'created', preview: '+ .atc/' };
207
+ }
208
+ function applyGitignore(cwd) {
209
+ const file = path.join(cwd, '.gitignore');
210
+ const existing = readText(file);
211
+ if (existing) {
212
+ const lines = existing.split('\n').map((l) => l.trim());
213
+ if (lines.includes('.atc/'))
214
+ return; // idempotent guard
215
+ const appended = existing.endsWith('\n') ? existing + '.atc/\n' : existing + '\n.atc/\n';
216
+ fs.writeFileSync(file, appended);
217
+ }
218
+ else {
219
+ fs.writeFileSync(file, '.atc/\n');
220
+ }
221
+ }
222
+ async function runInit(args) {
223
+ const cwd = process.cwd();
224
+ const json = args.json === true;
225
+ // -------------------------------------------------------------------------
226
+ // Step 1: Plan
227
+ // -------------------------------------------------------------------------
228
+ const mcpPlan = planMcp(cwd);
229
+ const agentsPlan = planAgentsMd(cwd);
230
+ const gitignorePlan = planGitignore(cwd);
231
+ const steps = [
232
+ { file: '.mcp.json', action: mcpPlan.action, preview: mcpPlan.preview },
233
+ { file: 'AGENTS.md', action: agentsPlan.action, preview: agentsPlan.preview },
234
+ { file: '.gitignore', action: gitignorePlan.action, preview: gitignorePlan.preview },
235
+ ];
236
+ if (!json) {
237
+ process.stdout.write('atc init — changes planned:\n\n');
238
+ for (const s of steps) {
239
+ const icon = s.action === 'unchanged' ? '·' : s.action === 'created' ? '+' : '~';
240
+ process.stdout.write(` ${icon} ${s.file.padEnd(14)} ${s.action}\n`);
241
+ if (s.preview && s.action !== 'unchanged') {
242
+ process.stdout.write(` ${s.preview}\n`);
243
+ }
244
+ }
245
+ process.stdout.write('\n');
246
+ }
247
+ const anyChanges = steps.some((s) => s.action !== 'unchanged');
248
+ if (!args.yes) {
249
+ if (!anyChanges) {
250
+ if (!json)
251
+ process.stdout.write('All files already up to date. Nothing to do.\n');
252
+ else
253
+ process.stdout.write(JSON.stringify({ actions: steps, checkin: null }, null, 2) + '\n');
254
+ return;
255
+ }
256
+ // Ask on stdin only when changes would be made
257
+ const proceed = await confirm('Proceed? [y/N] ');
258
+ if (!proceed) {
259
+ if (!json)
260
+ process.stdout.write('Aborted.\n');
261
+ else
262
+ process.stdout.write(JSON.stringify({ actions: steps, checkin: null, aborted: true }, null, 2) + '\n');
263
+ return;
264
+ }
265
+ }
266
+ if (!anyChanges) {
267
+ if (!json)
268
+ process.stdout.write('All files already up to date. Nothing to do.\n');
269
+ else
270
+ process.stdout.write(JSON.stringify({ actions: steps, checkin: null }, null, 2) + '\n');
271
+ return;
272
+ }
273
+ // -------------------------------------------------------------------------
274
+ // Step 2: .mcp.json
275
+ // -------------------------------------------------------------------------
276
+ if (mcpPlan.action !== 'unchanged') {
277
+ try {
278
+ applyMcp(cwd);
279
+ if (!json)
280
+ process.stdout.write(` wrote .mcp.json\n`);
281
+ }
282
+ catch (err) {
283
+ const msg = err instanceof Error ? err.message : String(err);
284
+ process.stderr.write(`atc: warning: could not write .mcp.json: ${msg}\n`);
285
+ }
286
+ }
287
+ // -------------------------------------------------------------------------
288
+ // Step 3: AGENTS.md
289
+ // -------------------------------------------------------------------------
290
+ if (agentsPlan.action !== 'unchanged') {
291
+ try {
292
+ applyAgentsMd(cwd);
293
+ if (!json)
294
+ process.stdout.write(` wrote AGENTS.md\n`);
295
+ }
296
+ catch (err) {
297
+ const msg = err instanceof Error ? err.message : String(err);
298
+ process.stderr.write(`atc: warning: could not write AGENTS.md: ${msg}\n`);
299
+ }
300
+ }
301
+ // Note about CLAUDE.md if it exists but doesn't reference AGENTS.md
302
+ try {
303
+ const claudeMd = readText(path.join(cwd, 'CLAUDE.md'));
304
+ if (claudeMd !== null &&
305
+ !claudeMd.includes('@AGENTS.md') &&
306
+ !claudeMd.includes(COMPLIANCE_MARKER)) {
307
+ if (!json) {
308
+ process.stdout.write('\n note: CLAUDE.md exists but does not include @AGENTS.md.\n' +
309
+ ' Consider adding "@AGENTS.md" to CLAUDE.md so Claude Code picks up the block.\n\n');
310
+ }
311
+ }
312
+ }
313
+ catch {
314
+ // best-effort
315
+ }
316
+ // -------------------------------------------------------------------------
317
+ // Step 4: .gitignore
318
+ // -------------------------------------------------------------------------
319
+ if (gitignorePlan.action !== 'unchanged') {
320
+ try {
321
+ applyGitignore(cwd);
322
+ if (!json)
323
+ process.stdout.write(` wrote .gitignore\n`);
324
+ }
325
+ catch (err) {
326
+ const msg = err instanceof Error ? err.message : String(err);
327
+ process.stderr.write(`atc: warning: could not write .gitignore: ${msg}\n`);
328
+ }
329
+ }
330
+ // -------------------------------------------------------------------------
331
+ // Step 5: Checkin + brief (fail-open)
332
+ // -------------------------------------------------------------------------
333
+ const token = args.token || process.env.ATC_TOKEN || '';
334
+ let checkinResult = null;
335
+ if (token) {
336
+ const cfg = (0, client_1.loadConfig)();
337
+ // If --token was passed, use it in-process only — never written to any file
338
+ const effectiveCfg = args.token ? { ...cfg, token: args.token } : cfg;
339
+ if (!json)
340
+ process.stdout.write('\nChecking in to Agent Tower...\n');
341
+ try {
342
+ const r = await (0, client_1.api)(effectiveCfg, effectiveCfg.token, 'POST', '/v1/sessions', {
343
+ workspaceId: (0, client_1.workspaceId)(),
344
+ cli: process.env.ATC_CLI || 'cli',
345
+ worktree: cwd,
346
+ branch: (0, client_1.currentBranch)(),
347
+ task: 'atc init',
348
+ });
349
+ if (r.ok) {
350
+ (0, client_1.saveSession)({ callsign: r.body.callsign, sessionToken: r.body.sessionToken, api: effectiveCfg.api });
351
+ const c = r.body.brief?.counts ?? {};
352
+ const standing = r.body.brief?.standing
353
+ ? `\n⚑ standing orders present (v${r.body.brief.standing.version}) — read them: atc standing`
354
+ : '';
355
+ checkinResult = r.body;
356
+ if (!json) {
357
+ process.stdout.write(`checked in as ${r.body.callsign} · ${c.sessions ?? 0} on board, ` +
358
+ `${c.claims ?? 0} claims, ${c.notams ?? 0} notams${standing}\n`);
359
+ }
360
+ }
361
+ else {
362
+ const msg = r.body?.error ?? `checkin failed (${r.status})`;
363
+ process.stderr.write(`atc: warning: could not check in: ${msg}\n`);
364
+ if (!json) {
365
+ process.stdout.write('\nNext steps:\n' +
366
+ ' 1. Set ATC_TOKEN to your frequency token (get one at https://app.agenttower.dev)\n' +
367
+ ' 2. Run: atc checkin\n');
368
+ }
369
+ }
370
+ }
371
+ catch (err) {
372
+ const msg = err instanceof Error ? err.message : String(err);
373
+ process.stderr.write(`atc: warning: checkin failed (network error): ${msg}\n`);
374
+ if (!json) {
375
+ process.stdout.write('\nNext steps:\n' +
376
+ ' 1. Ensure ATC_API is reachable (default: https://api.agenttower.dev)\n' +
377
+ ' 2. Set ATC_TOKEN to your frequency token\n' +
378
+ ' 3. Run: atc checkin\n');
379
+ }
380
+ }
381
+ }
382
+ else {
383
+ if (!json) {
384
+ process.stdout.write('\nDone. To land on the board:\n' +
385
+ ' 1. Get a frequency token at https://app.agenttower.dev\n' +
386
+ ' 2. export ATC_TOKEN=<your-token>\n' +
387
+ ' 3. Run: atc checkin\n');
388
+ }
389
+ }
390
+ if (json) {
391
+ process.stdout.write(JSON.stringify({ actions: steps, checkin: checkinResult }, null, 2) + '\n');
392
+ }
393
+ }
package/package.json CHANGED
@@ -1,16 +1,38 @@
1
1
  {
2
2
  "name": "@agenttower/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "atc — the Agent Tower CLI. Coordinate multiple coding agents on one repo (claims, conflicts, decision log).",
5
5
  "license": "MIT",
6
- "repository": { "type": "git", "url": "https://github.com/manateeit/agent-tower.git", "directory": "cli" },
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/manateeit/agent-tower.git",
9
+ "directory": "cli"
10
+ },
7
11
  "homepage": "https://app.agenttower.dev",
8
- "keywords": ["agent-tower", "atc", "coding-agents", "coordination", "cli", "claude", "mcp"],
12
+ "keywords": [
13
+ "agent-tower",
14
+ "atc",
15
+ "coding-agents",
16
+ "coordination",
17
+ "cli",
18
+ "claude",
19
+ "mcp"
20
+ ],
9
21
  "type": "commonjs",
10
- "bin": { "atc": "bin/atc.js" },
11
- "files": ["bin", "dist", "README.md"],
12
- "engines": { "node": ">=18" },
13
- "publishConfig": { "access": "public" },
22
+ "bin": {
23
+ "atc": "bin/atc.js"
24
+ },
25
+ "files": [
26
+ "bin",
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
14
36
  "scripts": {
15
37
  "build": "tsc",
16
38
  "typecheck": "tsc --noEmit",