@trekagent/claude 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -3
  2. package/bin/cli.mjs +198 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,11 @@ 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 and **pick a project** — the token (and the selected project) are then delivered straight
10
+ back to the installer over a localhost loopback listener and written as `TREK_TOKEN` /
11
+ `TREK_PROJECT_ID`. No copy-paste required.
12
+
8
13
  ## Usage
9
14
 
10
15
  ```bash
@@ -24,14 +29,30 @@ npx @trekagent/claude init --uninstall
24
29
  | --- | --- | --- |
25
30
  | `--project` | (default) | Project scope — write `./.claude/settings.local.json`. |
26
31
  | `--user` | | User scope — write `~/.claude/settings.local.json`. |
27
- | `--token <trk_...>` | `$TREK_TOKEN`, else prompt | Trek API token. |
32
+ | `--token <trk_...>` | browser login | Trek API token (skips the browser flow). |
28
33
  | `--api-url <url>` | `https://api.trekagent.io` | Trek API base URL. |
34
+ | `--cockpit-url <url>` | `https://console.trekagent.io` | Cockpit base URL used for browser login. |
29
35
  | `--project-id <uuid>` | `$TREK_PROJECT_ID` | Bind a default Trek project. |
30
36
  | `--marketplace <owner>/<repo>` | `trekagent/trek-claude-plugin` or `$TREK_MARKETPLACE` | GitHub repo hosting the plugin marketplace. |
37
+ | `--login` | | Force a fresh browser login, ignoring any saved token. |
38
+ | `--no-browser` | | Skip the browser flow and paste a token manually. |
31
39
  | `--uninstall` | | Remove the plugin + Trek env for the chosen scope. |
32
40
 
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.
41
+ ### How the token is resolved
42
+
43
+ `init` finds a token in this order:
44
+
45
+ 1. `--token <trk_...>` flag.
46
+ 2. `$TREK_TOKEN` environment variable.
47
+ 3. A `trk_` token already wired into project `./.claude/settings.local.json` or user
48
+ `~/.claude/settings.local.json` (skipped when `--login` is passed).
49
+ 4. **Browser login** (interactive terminals, unless `--no-browser`): opens the cockpit
50
+ `cli-auth` page, you sign in / sign up and select a project, and the token plus the chosen
51
+ project are delivered back automatically over a loopback listener bound to `127.0.0.1`. The
52
+ selected project is written as `TREK_PROJECT_ID` (an explicit `--project-id` still applies to
53
+ the non-browser paths).
54
+ 5. Manual paste prompt — the fallback for `--no-browser`, non-interactive shells, or if the
55
+ browser flow times out (3 min). Points you at **Settings → API tokens** in the cockpit.
35
56
 
36
57
  ## What `init` does
37
58
 
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,152 @@ 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 { token, projectId } on success, or
150
+ // null if it could not complete (timeout / browser failure / user error) so the
151
+ // caller can fall back. The cockpit success callback contract is:
152
+ // success: http://127.0.0.1:<port>/callback?token=<trk_...>&project=<projectId>&state=<nonce>
153
+ // error: http://127.0.0.1:<port>/callback?error=<msg>&state=<nonce>
154
+ // Project selection is mandatory in the cockpit, so `project` is normally present.
155
+ function browserLogin(flags) {
156
+ return new Promise((resolve) => {
157
+ const expectedState = randomUUID();
158
+ let settled = false;
159
+ let timer = null;
160
+
161
+ const finish = (result) => {
162
+ if (settled) return;
163
+ settled = true;
164
+ if (timer) clearTimeout(timer);
165
+ try { server.close(); } catch {}
166
+ resolve(result);
167
+ };
168
+
169
+ const server = createServer((req, res) => {
170
+ let url;
171
+ try { url = new URL(req.url, 'http://127.0.0.1'); }
172
+ catch { res.writeHead(204).end(); return; }
173
+
174
+ if (url.pathname !== '/callback') {
175
+ // favicon.ico and any other path: ignore.
176
+ res.writeHead(204).end();
177
+ return;
178
+ }
179
+
180
+ const token = url.searchParams.get('token');
181
+ const state = url.searchParams.get('state');
182
+ const error = url.searchParams.get('error');
183
+ const projectId = url.searchParams.get('project') || '';
184
+
185
+ // A mismatched state must NOT resolve — keep waiting.
186
+ if (state !== expectedState) {
187
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
188
+ res.end(errorHtml('Invalid state — this login request did not originate from this terminal.'));
189
+ return;
190
+ }
191
+
192
+ if (error) {
193
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
194
+ res.end(errorHtml(`Login failed: ${error}`));
195
+ warn(`browser login failed: ${error}`);
196
+ finish(null);
197
+ return;
198
+ }
199
+
200
+ if (looksLikeToken(token)) {
201
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
202
+ res.end(SUCCESS_HTML);
203
+ finish({ token, projectId });
204
+ return;
205
+ }
206
+
207
+ // Hit /callback with no usable token — show an error but keep waiting.
208
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
209
+ res.end(errorHtml('No token received.'));
210
+ });
211
+
212
+ server.on('error', (err) => {
213
+ warn(`could not start local login listener: ${err.message}`);
214
+ finish(null);
215
+ });
216
+
217
+ server.listen(0, '127.0.0.1', () => {
218
+ const port = server.address().port;
219
+ const label = `Claude Code @ ${hostname()}`;
220
+ const base = cockpitBase(flags);
221
+ const authUrl = `${base}/cli-auth?port=${port}&state=${encodeURIComponent(expectedState)}&name=${encodeURIComponent(label)}`;
222
+
223
+ log();
224
+ log(`Opening your browser to sign in to Trek…`);
225
+ const opened = openBrowser(authUrl);
226
+ if (!opened) {
227
+ log(`Could not open a browser automatically. Open this URL to continue:`);
228
+ log(` ${c.cyan(authUrl)}`);
229
+ } else {
230
+ log(c.dim(` If it doesn't open, visit: ${authUrl}`));
231
+ }
232
+ log(`Waiting for you to authorize in the browser… ${c.dim('(Ctrl+C to cancel)')}`);
233
+
234
+ timer = setTimeout(() => {
235
+ warn('timed out waiting for browser login.');
236
+ finish(null);
237
+ }, LOGIN_TIMEOUT_MS);
238
+ });
239
+ });
240
+ }
241
+
242
+ // Manual paste fallback (also used for --no-browser and non-TTY).
243
+ async function pasteToken() {
92
244
  log();
93
245
  log(`Trek needs an API token (${c.cyan('trk_...')}).`);
94
246
  log(`Mint one in the cockpit: ${c.cyan(COCKPIT_SETTINGS_URL)} (Settings → API tokens).`);
@@ -99,6 +251,30 @@ async function resolveToken(flags) {
99
251
  return tok;
100
252
  }
101
253
 
254
+ // Resolve a token (and, where available, a project id) following this precedence.
255
+ // Always returns { token, projectId }; projectId is '' for every path except a
256
+ // successful browser login, where the cockpit's mandatory project selection is
257
+ // delivered back over the loopback callback.
258
+ async function resolveToken(flags) {
259
+ // 1. explicit flag
260
+ if (typeof flags.token === 'string' && flags.token) return { token: flags.token, projectId: '' };
261
+ // 2. environment
262
+ if (process.env.TREK_TOKEN) { skip('using TREK_TOKEN from environment'); return { token: process.env.TREK_TOKEN, projectId: '' }; }
263
+ // 3. reuse an existing token from settings (unless --login forces re-auth)
264
+ if (!flags.login) {
265
+ const existing = detectExistingToken();
266
+ if (existing) { skip(`reusing TREK_TOKEN from ${rel(existing.path)}`); return { token: existing.token, projectId: '' }; }
267
+ }
268
+ // 4. browser login (interactive, unless --no-browser)
269
+ if (process.stdin.isTTY && !flags['no-browser']) {
270
+ const result = await browserLogin(flags);
271
+ if (result && looksLikeToken(result.token)) { ok('signed in via browser'); return { token: result.token, projectId: result.projectId || '' }; }
272
+ warn('falling back to manual token entry.');
273
+ }
274
+ // 5. manual paste fallback (also the --no-browser / non-TTY path)
275
+ return { token: await pasteToken(), projectId: '' };
276
+ }
277
+
102
278
  // --- settings.local.json: merge env block ----------------------------------
103
279
  function writeTokenEnv(claudeDir, apiUrl, token, projectId) {
104
280
  const path = join(claudeDir, 'settings.local.json');
@@ -149,7 +325,7 @@ function installPlugin(bin, marketplaceRepo) {
149
325
  async function init(flags) {
150
326
  const userScope = !!flags.user;
151
327
  const apiUrl = (typeof flags['api-url'] === 'string' && flags['api-url']) || DEFAULT_API_URL;
152
- const projectId = typeof flags['project-id'] === 'string' ? flags['project-id'] : process.env.TREK_PROJECT_ID || '';
328
+ const flagProjectId = typeof flags['project-id'] === 'string' ? flags['project-id'] : process.env.TREK_PROJECT_ID || '';
153
329
  const marketplaceRepo = (typeof flags.marketplace === 'string' && flags.marketplace) || DEFAULT_MARKETPLACE_REPO;
154
330
 
155
331
  log(c.bold(`\nTrek installer — ${userScope ? 'user' : 'project'} scope`));
@@ -165,7 +341,10 @@ async function init(flags) {
165
341
  installPlugin(bin, marketplaceRepo);
166
342
  }
167
343
 
168
- const token = await resolveToken(flags);
344
+ const { token, projectId: browserProjectId } = await resolveToken(flags);
345
+ // The browser flow's mandatory project selection wins; otherwise fall back to
346
+ // the explicit --project-id / $TREK_PROJECT_ID resolution.
347
+ const projectId = browserProjectId || flagProjectId;
169
348
  const claudeDir = userScope ? join(homedir(), '.claude') : join(CWD, '.claude');
170
349
  writeTokenEnv(claudeDir, apiUrl, token, projectId);
171
350
  if (!userScope) ensureGitignore(CWD, ['.claude/settings.local.json']);
@@ -211,20 +390,26 @@ ${c.bold('Usage')}
211
390
  ${c.bold('Options')}
212
391
  --project Project scope: write ./.claude/settings.local.json (default)
213
392
  --user User scope: write ~/.claude/settings.local.json
214
- --token <trk_...> Trek API token (else $TREK_TOKEN, else prompt)
393
+ --token <trk_...> Trek API token (skips browser login)
215
394
  --api-url <url> Trek API base URL (default ${DEFAULT_API_URL})
395
+ --cockpit-url <url> Cockpit base URL for browser login (default ${DEFAULT_COCKPIT_URL})
216
396
  --project-id <uuid> Bind a default Trek project (sets TREK_PROJECT_ID)
217
397
  --marketplace <o/r> GitHub owner/repo of the plugin marketplace (else $TREK_MARKETPLACE)
398
+ --login Force a fresh browser login (ignore any saved token)
399
+ --no-browser Skip browser login; paste a token manually
218
400
  --uninstall Remove the plugin + Trek env for the chosen scope
219
401
  -h, --help Show this help
220
402
 
221
403
  ${c.bold('What init does')}
222
404
  1. claude plugin marketplace add trekagent/trek-claude-plugin
223
405
  2. claude plugin install ${PLUGIN}@${MARKETPLACE}
224
- 3. writes TREK_TOKEN / TREK_API_URL into .claude/settings.local.json (gitignored)
406
+ 3. resolves a token (flag env → saved → browser login → manual paste)
407
+ 4. writes TREK_TOKEN / TREK_API_URL into .claude/settings.local.json (gitignored)
225
408
 
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.
409
+ By default, if no token is already available init opens your browser to sign in /
410
+ create an account; the token is created and delivered back automatically over a
411
+ localhost listener. The plugin ships the skill, presence hooks, and the remote MCP
412
+ server; the token in settings.local.json is what makes them authenticate. Re-running is safe.
228
413
  `;
229
414
 
230
415
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trekagent/claude",
3
- "version": "0.1.1",
3
+ "version": "0.3.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",