thalixtower-cli 0.6.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 +63 -0
- package/bin/atc.js +3 -0
- package/dist/client.js +125 -0
- package/dist/hook.js +440 -0
- package/dist/human.js +86 -0
- package/dist/index.js +736 -0
- package/dist/init.js +456 -0
- package/package.json +45 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
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 Thalix 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
|
+
## Thalix Tower coordination
|
|
58
|
+
|
|
59
|
+
You are one of several agents working this repository. Coordinate through the
|
|
60
|
+
**Thalix 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
|
+
- Every brief/squawk shows **traffic** (calls from other agents); when unacked
|
|
81
|
+
traffic appears, read it (\`atc_traffic\`) and answer what needs answering
|
|
82
|
+
(\`atc_call <callsign> "…"\`).
|
|
83
|
+
|
|
84
|
+
**When you settle a cross-cutting decision** (a contract, convention, TTL, etc.):
|
|
85
|
+
- \`atc_note "<the decision>"\` so the next agent reads it in their brief. Pin
|
|
86
|
+
durable contracts.
|
|
87
|
+
|
|
88
|
+
**When you finish / end the session:**
|
|
89
|
+
- \`atc_clear\` to release scope you no longer need; \`atc_checkout\` on session end.
|
|
90
|
+
|
|
91
|
+
Claims are **advisory** — the Tower detects and surfaces conflicts; it never blocks
|
|
92
|
+
your edits. Respect them anyway: that's the whole point.
|
|
93
|
+
|
|
94
|
+
<!-- CLI equivalents: atc checkin --task "…" · atc brief · atc standing · atc claim "<glob>" ·
|
|
95
|
+
atc squawk "…" [--code blocked|mayday] · atc note "…" [--pin] · atc note show <id> ·
|
|
96
|
+
atc call <callsign> "…" · atc traffic [--ack-all] ·
|
|
97
|
+
atc clear · atc checkout -->
|
|
98
|
+
`.trimStart();
|
|
99
|
+
const COMPLIANCE_MARKER = '## Thalix Tower coordination';
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// MCP entry we always want present
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const MCP_SERVER_KEY = 'thalix-tower';
|
|
104
|
+
const MCP_SERVER_ENTRY = {
|
|
105
|
+
command: 'npx',
|
|
106
|
+
args: ['-y', 'thalixtower-mcp'],
|
|
107
|
+
env: {
|
|
108
|
+
ATC_API: '${ATC_API}',
|
|
109
|
+
ATC_TOKEN: '${ATC_TOKEN}',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Helpers
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
function readText(file) {
|
|
116
|
+
try {
|
|
117
|
+
return fs.readFileSync(file, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function readJson(file) {
|
|
124
|
+
const text = readText(file);
|
|
125
|
+
if (!text)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Deep-equal check limited to what we need for the MCP entry. */
|
|
135
|
+
function mcpEntryMatches(existing) {
|
|
136
|
+
if (!existing || typeof existing !== 'object')
|
|
137
|
+
return false;
|
|
138
|
+
const e = existing;
|
|
139
|
+
if (e.command !== 'npx')
|
|
140
|
+
return false;
|
|
141
|
+
const args = e.args;
|
|
142
|
+
if (!Array.isArray(args))
|
|
143
|
+
return false;
|
|
144
|
+
return args.includes('thalixtower-mcp');
|
|
145
|
+
}
|
|
146
|
+
async function confirm(question) {
|
|
147
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
rl.question(question, (answer) => {
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function planMcp(cwd) {
|
|
156
|
+
const file = path.join(cwd, '.mcp.json');
|
|
157
|
+
const existing = readJson(file);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
return {
|
|
160
|
+
action: 'created',
|
|
161
|
+
preview: `+ mcpServers["${MCP_SERVER_KEY}"] = { command: "npx", args: ["-y", "thalixtower-mcp"], env: {...} }`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const servers = existing.mcpServers;
|
|
165
|
+
if (servers && mcpEntryMatches(servers[MCP_SERVER_KEY])) {
|
|
166
|
+
return { action: 'unchanged' };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
action: 'updated',
|
|
170
|
+
preview: `+ mcpServers["${MCP_SERVER_KEY}"] = { command: "npx", args: ["-y", "thalixtower-mcp"], env: {...} }`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function applyMcp(cwd) {
|
|
174
|
+
const file = path.join(cwd, '.mcp.json');
|
|
175
|
+
const existing = readJson(file) ?? {};
|
|
176
|
+
const servers = existing.mcpServers ?? {};
|
|
177
|
+
servers[MCP_SERVER_KEY] = MCP_SERVER_ENTRY;
|
|
178
|
+
const updated = { ...existing, mcpServers: servers };
|
|
179
|
+
fs.writeFileSync(file, JSON.stringify(updated, null, 2) + '\n');
|
|
180
|
+
}
|
|
181
|
+
function planAgentsMd(cwd) {
|
|
182
|
+
const file = path.join(cwd, 'AGENTS.md');
|
|
183
|
+
const existing = readText(file);
|
|
184
|
+
if (existing && existing.includes(COMPLIANCE_MARKER)) {
|
|
185
|
+
return { action: 'unchanged' };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
action: existing ? 'updated' : 'created',
|
|
189
|
+
preview: `+ ## Thalix Tower coordination\\n+ (compliance block, ~30 lines)`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function applyAgentsMd(cwd) {
|
|
193
|
+
const file = path.join(cwd, 'AGENTS.md');
|
|
194
|
+
const existing = readText(file);
|
|
195
|
+
if (existing && existing.includes(COMPLIANCE_MARKER))
|
|
196
|
+
return; // idempotent guard
|
|
197
|
+
const separator = existing && !existing.endsWith('\n\n') ? '\n' : '';
|
|
198
|
+
const content = existing ? existing + separator + COMPLIANCE_BLOCK : COMPLIANCE_BLOCK;
|
|
199
|
+
fs.writeFileSync(file, content);
|
|
200
|
+
}
|
|
201
|
+
function planGitignore(cwd, needed) {
|
|
202
|
+
const file = path.join(cwd, '.gitignore');
|
|
203
|
+
const existing = readText(file);
|
|
204
|
+
if (existing) {
|
|
205
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
206
|
+
const missing = needed.filter((n) => !lines.includes(n));
|
|
207
|
+
if (missing.length === 0)
|
|
208
|
+
return { action: 'unchanged' };
|
|
209
|
+
return { action: 'updated', preview: missing.map((m) => `+ ${m}`).join(' ') };
|
|
210
|
+
}
|
|
211
|
+
return { action: 'created', preview: needed.map((m) => `+ ${m}`).join(' ') };
|
|
212
|
+
}
|
|
213
|
+
function applyGitignore(cwd, needed) {
|
|
214
|
+
const file = path.join(cwd, '.gitignore');
|
|
215
|
+
const existing = readText(file);
|
|
216
|
+
const lines = (existing ?? '').split('\n').map((l) => l.trim());
|
|
217
|
+
const missing = needed.filter((n) => !lines.includes(n));
|
|
218
|
+
if (existing && missing.length === 0)
|
|
219
|
+
return; // idempotent guard
|
|
220
|
+
const base = existing ? (existing.endsWith('\n') ? existing : existing + '\n') : '';
|
|
221
|
+
fs.writeFileSync(file, base + missing.join('\n') + '\n');
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// --save-token: persist ATC_TOKEN into .claude/settings.local.json so this
|
|
225
|
+
// repo's Claude Code sessions get their own frequency without a global export.
|
|
226
|
+
// The file is local-only and we make sure it's gitignored above.
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
const SETTINGS_REL = path.join('.claude', 'settings.local.json');
|
|
229
|
+
function planSettings(cwd, token) {
|
|
230
|
+
const file = path.join(cwd, SETTINGS_REL);
|
|
231
|
+
const existing = readJson(file);
|
|
232
|
+
const env = existing?.env ?? {};
|
|
233
|
+
const masked = `ATC_TOKEN=atcf_…${token.slice(-4)}`;
|
|
234
|
+
if (env.ATC_TOKEN === token)
|
|
235
|
+
return { action: 'unchanged' };
|
|
236
|
+
return { action: existing ? 'updated' : 'created', preview: `+ env.${masked}` };
|
|
237
|
+
}
|
|
238
|
+
function applySettings(cwd, token, apiUrl) {
|
|
239
|
+
const file = path.join(cwd, SETTINGS_REL);
|
|
240
|
+
const existing = readJson(file) ?? {};
|
|
241
|
+
const env = (existing.env ?? {});
|
|
242
|
+
env.ATC_TOKEN = token;
|
|
243
|
+
if (apiUrl)
|
|
244
|
+
env.ATC_API = apiUrl; // only when targeting a non-default API
|
|
245
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
246
|
+
fs.writeFileSync(file, JSON.stringify({ ...existing, env }, null, 2) + '\n', { mode: 0o600 });
|
|
247
|
+
try {
|
|
248
|
+
fs.chmodSync(file, 0o600);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* best-effort on platforms without chmod */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function runInit(args) {
|
|
255
|
+
const cwd = process.cwd();
|
|
256
|
+
const json = args.json === true;
|
|
257
|
+
const token = args.token || process.env.ATC_TOKEN || '';
|
|
258
|
+
if (args.saveToken && !token) {
|
|
259
|
+
process.stderr.write("atc: --save-token needs a token — pass --token <atcf_…> or set ATC_TOKEN\n");
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
// -------------------------------------------------------------------------
|
|
263
|
+
// Step 1: Plan
|
|
264
|
+
// -------------------------------------------------------------------------
|
|
265
|
+
const gitignoreLines = ['.atc/', ...(args.saveToken ? ['.claude/settings.local.json'] : [])];
|
|
266
|
+
const mcpPlan = planMcp(cwd);
|
|
267
|
+
const agentsPlan = planAgentsMd(cwd);
|
|
268
|
+
const gitignorePlan = planGitignore(cwd, gitignoreLines);
|
|
269
|
+
const settingsPlan = args.saveToken ? planSettings(cwd, token) : null;
|
|
270
|
+
const steps = [
|
|
271
|
+
{ file: '.mcp.json', action: mcpPlan.action, preview: mcpPlan.preview },
|
|
272
|
+
{ file: 'AGENTS.md', action: agentsPlan.action, preview: agentsPlan.preview },
|
|
273
|
+
{ file: '.gitignore', action: gitignorePlan.action, preview: gitignorePlan.preview },
|
|
274
|
+
...(settingsPlan
|
|
275
|
+
? [{ file: SETTINGS_REL, action: settingsPlan.action, preview: settingsPlan.preview }]
|
|
276
|
+
: []),
|
|
277
|
+
];
|
|
278
|
+
if (!json) {
|
|
279
|
+
process.stdout.write('atc init — changes planned:\n\n');
|
|
280
|
+
for (const s of steps) {
|
|
281
|
+
const icon = s.action === 'unchanged' ? '·' : s.action === 'created' ? '+' : '~';
|
|
282
|
+
process.stdout.write(` ${icon} ${s.file.padEnd(14)} ${s.action}\n`);
|
|
283
|
+
if (s.preview && s.action !== 'unchanged') {
|
|
284
|
+
process.stdout.write(` ${s.preview}\n`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
process.stdout.write('\n');
|
|
288
|
+
}
|
|
289
|
+
const anyChanges = steps.some((s) => s.action !== 'unchanged');
|
|
290
|
+
if (!args.yes) {
|
|
291
|
+
if (!anyChanges) {
|
|
292
|
+
if (!json)
|
|
293
|
+
process.stdout.write('All files already up to date. Nothing to do.\n');
|
|
294
|
+
else
|
|
295
|
+
process.stdout.write(JSON.stringify({ actions: steps, checkin: null }, null, 2) + '\n');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Ask on stdin only when changes would be made
|
|
299
|
+
const proceed = await confirm('Proceed? [y/N] ');
|
|
300
|
+
if (!proceed) {
|
|
301
|
+
if (!json)
|
|
302
|
+
process.stdout.write('Aborted.\n');
|
|
303
|
+
else
|
|
304
|
+
process.stdout.write(JSON.stringify({ actions: steps, checkin: null, aborted: true }, null, 2) + '\n');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!anyChanges) {
|
|
309
|
+
if (!json)
|
|
310
|
+
process.stdout.write('All files already up to date. Nothing to do.\n');
|
|
311
|
+
else
|
|
312
|
+
process.stdout.write(JSON.stringify({ actions: steps, checkin: null }, null, 2) + '\n');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// -------------------------------------------------------------------------
|
|
316
|
+
// Step 2: .mcp.json
|
|
317
|
+
// -------------------------------------------------------------------------
|
|
318
|
+
if (mcpPlan.action !== 'unchanged') {
|
|
319
|
+
try {
|
|
320
|
+
applyMcp(cwd);
|
|
321
|
+
if (!json)
|
|
322
|
+
process.stdout.write(` wrote .mcp.json\n`);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
326
|
+
process.stderr.write(`atc: warning: could not write .mcp.json: ${msg}\n`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
// Step 3: AGENTS.md
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
332
|
+
if (agentsPlan.action !== 'unchanged') {
|
|
333
|
+
try {
|
|
334
|
+
applyAgentsMd(cwd);
|
|
335
|
+
if (!json)
|
|
336
|
+
process.stdout.write(` wrote AGENTS.md\n`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
340
|
+
process.stderr.write(`atc: warning: could not write AGENTS.md: ${msg}\n`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Note about CLAUDE.md if it exists but doesn't reference AGENTS.md
|
|
344
|
+
try {
|
|
345
|
+
const claudeMd = readText(path.join(cwd, 'CLAUDE.md'));
|
|
346
|
+
if (claudeMd !== null &&
|
|
347
|
+
!claudeMd.includes('@AGENTS.md') &&
|
|
348
|
+
!claudeMd.includes(COMPLIANCE_MARKER)) {
|
|
349
|
+
if (!json) {
|
|
350
|
+
process.stdout.write('\n note: CLAUDE.md exists but does not include @AGENTS.md.\n' +
|
|
351
|
+
' Consider adding "@AGENTS.md" to CLAUDE.md so Claude Code picks up the block.\n\n');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// best-effort
|
|
357
|
+
}
|
|
358
|
+
// -------------------------------------------------------------------------
|
|
359
|
+
// Step 4: .gitignore
|
|
360
|
+
// -------------------------------------------------------------------------
|
|
361
|
+
if (gitignorePlan.action !== 'unchanged') {
|
|
362
|
+
try {
|
|
363
|
+
applyGitignore(cwd, gitignoreLines);
|
|
364
|
+
if (!json)
|
|
365
|
+
process.stdout.write(` wrote .gitignore\n`);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
369
|
+
process.stderr.write(`atc: warning: could not write .gitignore: ${msg}\n`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// Step 5: --save-token → .claude/settings.local.json (gitignored above)
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
if (settingsPlan && settingsPlan.action !== 'unchanged') {
|
|
376
|
+
try {
|
|
377
|
+
const cfgForApi = (0, client_1.loadConfig)();
|
|
378
|
+
const apiOverride = process.env.ATC_API ? cfgForApi.api : undefined;
|
|
379
|
+
applySettings(cwd, token, apiOverride);
|
|
380
|
+
if (!json)
|
|
381
|
+
process.stdout.write(` wrote ${SETTINGS_REL} (token saved for this repo's sessions)\n`);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
385
|
+
process.stderr.write(`atc: warning: could not write ${SETTINGS_REL}: ${msg}\n`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
// Step 6: Checkin + brief (fail-open)
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
let checkinResult = null;
|
|
392
|
+
if (token) {
|
|
393
|
+
const cfg = (0, client_1.loadConfig)();
|
|
394
|
+
// If --token was passed, use it in-process only — never written to any file
|
|
395
|
+
const effectiveCfg = args.token ? { ...cfg, token: args.token } : cfg;
|
|
396
|
+
if (!json)
|
|
397
|
+
process.stdout.write('\nChecking in to Thalix Tower...\n');
|
|
398
|
+
try {
|
|
399
|
+
const r = await (0, client_1.api)(effectiveCfg, effectiveCfg.token, 'POST', '/v1/sessions', {
|
|
400
|
+
workspaceId: (0, client_1.workspaceId)(),
|
|
401
|
+
cli: process.env.ATC_CLI || 'cli',
|
|
402
|
+
worktree: cwd,
|
|
403
|
+
branch: (0, client_1.currentBranch)(),
|
|
404
|
+
task: 'atc init',
|
|
405
|
+
});
|
|
406
|
+
if (r.ok) {
|
|
407
|
+
(0, client_1.saveSession)({ callsign: r.body.callsign, sessionToken: r.body.sessionToken, api: effectiveCfg.api });
|
|
408
|
+
const c = r.body.brief?.counts ?? {};
|
|
409
|
+
const so = r.body.brief?.standing;
|
|
410
|
+
const standing = so
|
|
411
|
+
? `\n⚑ standing orders present (${(so.orders ?? []).map((o) => o.name).join(', ') || 'assigned'}) — read them: atc standing`
|
|
412
|
+
: '';
|
|
413
|
+
checkinResult = r.body;
|
|
414
|
+
if (!json) {
|
|
415
|
+
process.stdout.write(`checked in as ${r.body.callsign} · ${c.sessions ?? 0} on board, ` +
|
|
416
|
+
`${c.claims ?? 0} claims, ${c.notams ?? 0} notams${standing}\n`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
const msg = r.body?.error ?? `checkin failed (${r.status})`;
|
|
421
|
+
process.stderr.write(`atc: warning: could not check in: ${msg}\n`);
|
|
422
|
+
if (!json) {
|
|
423
|
+
process.stdout.write('\nNext steps:\n' +
|
|
424
|
+
' 1. Set ATC_TOKEN to your frequency token (get one at https://tower.thalixinc.ai)\n' +
|
|
425
|
+
' 2. Run: atc checkin\n');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
431
|
+
process.stderr.write(`atc: warning: checkin failed (network error): ${msg}\n`);
|
|
432
|
+
if (!json) {
|
|
433
|
+
process.stdout.write('\nNext steps:\n' +
|
|
434
|
+
' 1. Ensure ATC_API is reachable (default: https://api.tower.thalixinc.ai)\n' +
|
|
435
|
+
' 2. Set ATC_TOKEN to your frequency token\n' +
|
|
436
|
+
' 3. Run: atc checkin\n');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
if (!json) {
|
|
442
|
+
process.stdout.write('\nDone. To land on the board:\n' +
|
|
443
|
+
' 1. Get a frequency token at https://tower.thalixinc.ai\n' +
|
|
444
|
+
' 2. export ATC_TOKEN=<your-token>\n' +
|
|
445
|
+
' 3. Run: atc checkin\n');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!json) {
|
|
449
|
+
process.stdout.write("\ntip: 'atc hook install' wires Claude Code hooks — brief injection at session\n" +
|
|
450
|
+
'start, auto-claims on every edit, traffic at every prompt. Compliance without\n' +
|
|
451
|
+
'the model having to remember anything.\n');
|
|
452
|
+
}
|
|
453
|
+
if (json) {
|
|
454
|
+
process.stdout.write(JSON.stringify({ actions: steps, checkin: checkinResult }, null, 2) + '\n');
|
|
455
|
+
}
|
|
456
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thalixtower-cli",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "atc — the Thalix Tower CLI. Coordinate multiple coding agents on one repo (claims, conflicts, decision log).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/manateeit/thalix-tower.git",
|
|
9
|
+
"directory": "cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://tower.thalixinc.ai",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"thalix-tower",
|
|
14
|
+
"atc",
|
|
15
|
+
"coding-agents",
|
|
16
|
+
"coordination",
|
|
17
|
+
"cli",
|
|
18
|
+
"claude",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
21
|
+
"type": "commonjs",
|
|
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
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.16.10",
|
|
43
|
+
"typescript": "^5.6.2"
|
|
44
|
+
}
|
|
45
|
+
}
|