@agenttower/cli 0.1.1 → 0.3.1

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