@trekagent/claude 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/bin/cli.mjs +184 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,10 @@ plugin** (skill + presence hooks + remote MCP server) from the Trek marketplace
5
5
  API token — so an agent starts reporting presence and working the ready-task frontier the moment
6
6
  you restart Claude Code.
7
7
 
8
+ If no token is already available, `init` **opens your browser** so you can sign in or create an
9
+ account — the token is then created and delivered straight back to the installer over a localhost
10
+ loopback listener. No copy-paste required.
11
+
8
12
  ## Usage
9
13
 
10
14
  ```bash
@@ -24,14 +28,28 @@ npx @trekagent/claude init --uninstall
24
28
  | --- | --- | --- |
25
29
  | `--project` | (default) | Project scope — write `./.claude/settings.local.json`. |
26
30
  | `--user` | | User scope — write `~/.claude/settings.local.json`. |
27
- | `--token <trk_...>` | `$TREK_TOKEN`, else prompt | Trek API token. |
31
+ | `--token <trk_...>` | browser login | Trek API token (skips the browser flow). |
28
32
  | `--api-url <url>` | `https://api.trekagent.io` | Trek API base URL. |
33
+ | `--cockpit-url <url>` | `https://console.trekagent.io` | Cockpit base URL used for browser login. |
29
34
  | `--project-id <uuid>` | `$TREK_PROJECT_ID` | Bind a default Trek project. |
30
35
  | `--marketplace <owner>/<repo>` | `trekagent/trek-claude-plugin` or `$TREK_MARKETPLACE` | GitHub repo hosting the plugin marketplace. |
36
+ | `--login` | | Force a fresh browser login, ignoring any saved token. |
37
+ | `--no-browser` | | Skip the browser flow and paste a token manually. |
31
38
  | `--uninstall` | | Remove the plugin + Trek env for the chosen scope. |
32
39
 
33
- If no token is provided via flag or env, `init` prompts for one (TTY) and points you at the
34
- cockpit **Settings → API tokens** page to mint one.
40
+ ### How the token is resolved
41
+
42
+ `init` finds a token in this order:
43
+
44
+ 1. `--token <trk_...>` flag.
45
+ 2. `$TREK_TOKEN` environment variable.
46
+ 3. A `trk_` token already wired into project `./.claude/settings.local.json` or user
47
+ `~/.claude/settings.local.json` (skipped when `--login` is passed).
48
+ 4. **Browser login** (interactive terminals, unless `--no-browser`): opens the cockpit
49
+ `cli-auth` page, you sign in / sign up, and the token is delivered back automatically over a
50
+ loopback listener bound to `127.0.0.1`.
51
+ 5. Manual paste prompt — the fallback for `--no-browser`, non-interactive shells, or if the
52
+ browser flow times out (3 min). Points you at **Settings → API tokens** in the cockpit.
35
53
 
36
54
  ## What `init` does
37
55
 
package/bin/cli.mjs CHANGED
@@ -4,22 +4,31 @@
4
4
  // Installs the Trek plugin (skill + presence hooks + remote MCP server) from the Trek
5
5
  // marketplace, then wires your API token so the MCP server and hooks authenticate.
6
6
  //
7
+ // By default, if no token is already available, init opens your browser to sign in and
8
+ // the token is created + delivered back automatically over a localhost loopback listener.
9
+ //
7
10
  // npx @trekagent/claude init # project scope (writes ./.claude/settings.local.json)
8
11
  // npx @trekagent/claude init --user # user scope (writes ~/.claude/settings.local.json)
9
12
  // npx @trekagent/claude init --token trk_... --api-url https://api.trekagent.io
13
+ // npx @trekagent/claude init --login # force a fresh browser login
14
+ // npx @trekagent/claude init --no-browser # skip browser, paste a token manually
10
15
  // npx @trekagent/claude init --marketplace owner/repo # override the marketplace source
11
16
  // npx @trekagent/claude init --uninstall
12
17
  //
13
18
  // Pure Node, no deps. Idempotent: re-running never duplicates or clobbers your other settings.
14
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
20
  import { dirname, join } from 'node:path';
16
- import { homedir } from 'node:os';
21
+ import { homedir, hostname } from 'node:os';
17
22
  import { createInterface } from 'node:readline';
18
- import { spawnSync } from 'node:child_process';
23
+ import { spawnSync, spawn } from 'node:child_process';
24
+ import { createServer } from 'node:http';
25
+ import { randomUUID } from 'node:crypto';
19
26
 
20
27
  // --- defaults --------------------------------------------------------------
21
28
  const DEFAULT_API_URL = 'https://api.trekagent.io';
22
- const COCKPIT_SETTINGS_URL = 'https://console.trekagent.io/settings';
29
+ const DEFAULT_COCKPIT_URL = 'https://console.trekagent.io';
30
+ const COCKPIT_SETTINGS_URL = `${DEFAULT_COCKPIT_URL}/settings`;
31
+ const LOGIN_TIMEOUT_MS = 180000; // 3 min to authorize in the browser
23
32
  const PLUGIN = 'trek';
24
33
  const MARKETPLACE = 'trek'; // the `name` in the marketplace's marketplace.json
25
34
  // GitHub org/repo hosting the plugin marketplace.
@@ -42,7 +51,7 @@ const warn = (s) => log(` ${c.yellow('!!')} ${s}`);
42
51
  // --- arg parsing -----------------------------------------------------------
43
52
  function parseArgs(argv) {
44
53
  const args = { _: [], flags: {} };
45
- const bools = new Set(['user', 'project', 'uninstall', 'help', 'force']);
54
+ const bools = new Set(['user', 'project', 'uninstall', 'help', 'force', 'no-browser', 'login']);
46
55
  for (let i = 0; i < argv.length; i++) {
47
56
  const a = argv[i];
48
57
  if (a === '-h') { args.flags.help = true; continue; }
@@ -86,9 +95,147 @@ function claude(bin, args) {
86
95
  }
87
96
 
88
97
  // --- token -----------------------------------------------------------------
89
- async function resolveToken(flags) {
90
- if (typeof flags.token === 'string' && flags.token) return flags.token;
91
- if (process.env.TREK_TOKEN) { skip('using TREK_TOKEN from environment'); return process.env.TREK_TOKEN; }
98
+ const looksLikeToken = (t) => typeof t === 'string' && t.startsWith('trk_');
99
+
100
+ // Derive the cockpit base URL: explicit --cockpit-url wins, otherwise fall back
101
+ // to the default cockpit (used both for the default api base and any custom one).
102
+ function cockpitBase(flags) {
103
+ if (typeof flags['cockpit-url'] === 'string' && flags['cockpit-url']) {
104
+ return flags['cockpit-url'].replace(/\/+$/, '');
105
+ }
106
+ return DEFAULT_COCKPIT_URL;
107
+ }
108
+
109
+ // Reuse a token already wired into project or user settings.local.json.
110
+ function detectExistingToken() {
111
+ const candidates = [
112
+ join(CWD, '.claude', 'settings.local.json'),
113
+ join(homedir(), '.claude', 'settings.local.json'),
114
+ ];
115
+ for (const path of candidates) {
116
+ const cur = readJson(path);
117
+ const tok = cur && cur.env && cur.env.TREK_TOKEN;
118
+ if (looksLikeToken(tok)) return { token: tok, path };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ // Open a URL in the user's default browser, cross-platform. Tolerates failure.
124
+ function openBrowser(url) {
125
+ try {
126
+ let cmd, args;
127
+ if (process.platform === 'darwin') { cmd = 'open'; args = [url]; }
128
+ else if (process.platform === 'win32') { cmd = 'cmd'; args = ['/c', 'start', '', url]; }
129
+ else { cmd = 'xdg-open'; args = [url]; }
130
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
131
+ child.on('error', () => {});
132
+ child.unref();
133
+ return true;
134
+ } catch { return false; }
135
+ }
136
+
137
+ const SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Trek CLI connected</title>
138
+ <style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0d10;color:#e8eaed;
139
+ display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
140
+ .card{text-align:center;max-width:30rem;padding:2rem}h1{font-size:1.4rem}p{color:#9aa0a6}</style></head>
141
+ <body><div class="card"><h1>Trek CLI connected ✓</h1>
142
+ <p>You can close this tab and return to your terminal.</p></div></body></html>`;
143
+ const errorHtml = (msg) => `<!doctype html><html><head><meta charset="utf-8"><title>Trek CLI</title>
144
+ <style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0d10;color:#e8eaed;
145
+ display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
146
+ .card{text-align:center;max-width:30rem;padding:2rem}h1{font-size:1.4rem}p{color:#f28b82}</style></head>
147
+ <body><div class="card"><h1>Trek CLI</h1><p>${String(msg).replace(/[<>&]/g, '')}</p></div></body></html>`;
148
+
149
+ // Loopback browser-login flow. Resolves with a trk_ token, or null if it could
150
+ // not complete (timeout / browser failure / user error) so the caller can fall back.
151
+ function browserLogin(flags) {
152
+ return new Promise((resolve) => {
153
+ const expectedState = randomUUID();
154
+ let settled = false;
155
+ let timer = null;
156
+
157
+ const finish = (result) => {
158
+ if (settled) return;
159
+ settled = true;
160
+ if (timer) clearTimeout(timer);
161
+ try { server.close(); } catch {}
162
+ resolve(result);
163
+ };
164
+
165
+ const server = createServer((req, res) => {
166
+ let url;
167
+ try { url = new URL(req.url, 'http://127.0.0.1'); }
168
+ catch { res.writeHead(204).end(); return; }
169
+
170
+ if (url.pathname !== '/callback') {
171
+ // favicon.ico and any other path: ignore.
172
+ res.writeHead(204).end();
173
+ return;
174
+ }
175
+
176
+ const token = url.searchParams.get('token');
177
+ const state = url.searchParams.get('state');
178
+ const error = url.searchParams.get('error');
179
+
180
+ // A mismatched state must NOT resolve — keep waiting.
181
+ if (state !== expectedState) {
182
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
183
+ res.end(errorHtml('Invalid state — this login request did not originate from this terminal.'));
184
+ return;
185
+ }
186
+
187
+ if (error) {
188
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
189
+ res.end(errorHtml(`Login failed: ${error}`));
190
+ warn(`browser login failed: ${error}`);
191
+ finish(null);
192
+ return;
193
+ }
194
+
195
+ if (looksLikeToken(token)) {
196
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
197
+ res.end(SUCCESS_HTML);
198
+ finish(token);
199
+ return;
200
+ }
201
+
202
+ // Hit /callback with no usable token — show an error but keep waiting.
203
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
204
+ res.end(errorHtml('No token received.'));
205
+ });
206
+
207
+ server.on('error', (err) => {
208
+ warn(`could not start local login listener: ${err.message}`);
209
+ finish(null);
210
+ });
211
+
212
+ server.listen(0, '127.0.0.1', () => {
213
+ const port = server.address().port;
214
+ const label = `Claude Code @ ${hostname()}`;
215
+ const base = cockpitBase(flags);
216
+ const authUrl = `${base}/cli-auth?port=${port}&state=${encodeURIComponent(expectedState)}&name=${encodeURIComponent(label)}`;
217
+
218
+ log();
219
+ log(`Opening your browser to sign in to Trek…`);
220
+ const opened = openBrowser(authUrl);
221
+ if (!opened) {
222
+ log(`Could not open a browser automatically. Open this URL to continue:`);
223
+ log(` ${c.cyan(authUrl)}`);
224
+ } else {
225
+ log(c.dim(` If it doesn't open, visit: ${authUrl}`));
226
+ }
227
+ log(`Waiting for you to authorize in the browser… ${c.dim('(Ctrl+C to cancel)')}`);
228
+
229
+ timer = setTimeout(() => {
230
+ warn('timed out waiting for browser login.');
231
+ finish(null);
232
+ }, LOGIN_TIMEOUT_MS);
233
+ });
234
+ });
235
+ }
236
+
237
+ // Manual paste fallback (also used for --no-browser and non-TTY).
238
+ async function pasteToken() {
92
239
  log();
93
240
  log(`Trek needs an API token (${c.cyan('trk_...')}).`);
94
241
  log(`Mint one in the cockpit: ${c.cyan(COCKPIT_SETTINGS_URL)} (Settings → API tokens).`);
@@ -99,6 +246,26 @@ async function resolveToken(flags) {
99
246
  return tok;
100
247
  }
101
248
 
249
+ async function resolveToken(flags) {
250
+ // 1. explicit flag
251
+ if (typeof flags.token === 'string' && flags.token) return flags.token;
252
+ // 2. environment
253
+ if (process.env.TREK_TOKEN) { skip('using TREK_TOKEN from environment'); return process.env.TREK_TOKEN; }
254
+ // 3. reuse an existing token from settings (unless --login forces re-auth)
255
+ if (!flags.login) {
256
+ const existing = detectExistingToken();
257
+ if (existing) { skip(`reusing TREK_TOKEN from ${rel(existing.path)}`); return existing.token; }
258
+ }
259
+ // 4. browser login (interactive, unless --no-browser)
260
+ if (process.stdin.isTTY && !flags['no-browser']) {
261
+ const tok = await browserLogin(flags);
262
+ if (looksLikeToken(tok)) { ok('signed in via browser'); return tok; }
263
+ warn('falling back to manual token entry.');
264
+ }
265
+ // 5. manual paste fallback (also the --no-browser / non-TTY path)
266
+ return pasteToken();
267
+ }
268
+
102
269
  // --- settings.local.json: merge env block ----------------------------------
103
270
  function writeTokenEnv(claudeDir, apiUrl, token, projectId) {
104
271
  const path = join(claudeDir, 'settings.local.json');
@@ -211,20 +378,26 @@ ${c.bold('Usage')}
211
378
  ${c.bold('Options')}
212
379
  --project Project scope: write ./.claude/settings.local.json (default)
213
380
  --user User scope: write ~/.claude/settings.local.json
214
- --token <trk_...> Trek API token (else $TREK_TOKEN, else prompt)
381
+ --token <trk_...> Trek API token (skips browser login)
215
382
  --api-url <url> Trek API base URL (default ${DEFAULT_API_URL})
383
+ --cockpit-url <url> Cockpit base URL for browser login (default ${DEFAULT_COCKPIT_URL})
216
384
  --project-id <uuid> Bind a default Trek project (sets TREK_PROJECT_ID)
217
385
  --marketplace <o/r> GitHub owner/repo of the plugin marketplace (else $TREK_MARKETPLACE)
386
+ --login Force a fresh browser login (ignore any saved token)
387
+ --no-browser Skip browser login; paste a token manually
218
388
  --uninstall Remove the plugin + Trek env for the chosen scope
219
389
  -h, --help Show this help
220
390
 
221
391
  ${c.bold('What init does')}
222
392
  1. claude plugin marketplace add trekagent/trek-claude-plugin
223
393
  2. claude plugin install ${PLUGIN}@${MARKETPLACE}
224
- 3. writes TREK_TOKEN / TREK_API_URL into .claude/settings.local.json (gitignored)
394
+ 3. resolves a token (flag env → saved → browser login → manual paste)
395
+ 4. writes TREK_TOKEN / TREK_API_URL into .claude/settings.local.json (gitignored)
225
396
 
226
- The plugin ships the skill, presence hooks, and the remote MCP server; the token in
227
- settings.local.json is what makes them authenticate. Re-running is safe.
397
+ By default, if no token is already available init opens your browser to sign in /
398
+ create an account; the token is created and delivered back automatically over a
399
+ localhost listener. The plugin ships the skill, presence hooks, and the remote MCP
400
+ server; the token in settings.local.json is what makes them authenticate. Re-running is safe.
228
401
  `;
229
402
 
230
403
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trekagent/claude",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One-command installer for Trek: installs the Trek Claude Code plugin (skill + hooks + MCP) from the Trek marketplace and wires your API token.",
5
5
  "private": false,
6
6
  "type": "module",