@kendoo.agentdesk/agentdesk 0.17.6 → 0.18.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/CHANGELOG.md CHANGED
@@ -8,6 +8,22 @@ All user-facing changes to AgentDesk. Each entry is tagged:
8
8
 
9
9
  Internal refactors, infrastructure changes, and architectural notes are not listed here.
10
10
 
11
+ ## [0.18.1] — 2026-04-18
12
+
13
+ ### Fixed
14
+ - `[CLI]` Jira permission check now trims a trailing slash from the stored base URL (a leftover double-slash could produce a spurious 404) and falls back to REST API v2 if v3 isn't exposed. The error message now includes the exact URL called so misconfigs are easier to diagnose.
15
+
16
+ ## [0.18.0] — 2026-04-18
17
+
18
+ ### Changed
19
+ - `[CLI]` `agentdesk init` redesigned:
20
+ - Tracker first, then its location, then auth, then verify, then pick a project from a list fetched from the tracker API (Linear teams, Jira projects). No more asking for a project key before you've told the wizard what tracker you're using.
21
+ - Every credential step prints exact steps: the URL to open, the button to click, the label to use, the scopes to pick.
22
+ - Always asks for GitHub repo, account handle, and token — even when the tracker is Linear, Jira, or None. Agents push code via GitHub; this is no longer left to luck or a global `gh auth`.
23
+ - Always asks for an identity badge (optional display name).
24
+ - The agentdesk project key is asked last, defaulting to the tracker's project key so you don't type it twice.
25
+ - Verifies the GitHub token matches the account handle you typed. Warns on mismatch.
26
+
11
27
  ## [0.17.6] — 2026-04-18
12
28
 
13
29
  ### Fixed
package/cli/config.mjs CHANGED
@@ -16,7 +16,7 @@ const DEFAULTS = {
16
16
  tracker: null,
17
17
  linear: { teamKey: null, workspace: null },
18
18
  jira: { baseUrl: null, project: null },
19
- github: { repo: null },
19
+ github: { repo: null, login: null },
20
20
  team: null,
21
21
  commands: { test: null, build: null, lint: null },
22
22
  projectAgents: [],
package/cli/init.mjs CHANGED
@@ -155,6 +155,131 @@ async function fetchServerCreds(apiKey, projectKey) {
155
155
  return {};
156
156
  }
157
157
 
158
+ // Render a numbered-step box of instructions with exact URLs and button names.
159
+ function printSteps(title, steps) {
160
+ const pad = " ";
161
+ console.log(`${pad}${title}`);
162
+ console.log(`${pad}${"─".repeat(title.length)}`);
163
+ for (let i = 0; i < steps.length; i++) {
164
+ console.log(`${pad} ${i + 1}. ${steps[i]}`);
165
+ }
166
+ console.log("");
167
+ }
168
+
169
+ // Try to detect the current git remote (origin) and parse owner/repo from
170
+ // known GitHub URL forms. Returns "owner/repo" or null. Used as a default
171
+ // suggestion in the GitHub step.
172
+ function detectGitRemote(dir) {
173
+ try {
174
+ const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
175
+ let m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
176
+ if (m) return `${m[1]}/${m[2]}`;
177
+ } catch {}
178
+ return null;
179
+ }
180
+
181
+ // Fetch the set of "things to pick from" per tracker once the auth has been
182
+ // verified. For Linear this is teams; Jira is projects; GitHub Issues is
183
+ // repos (filtered to ones the token can access). Returns an array of
184
+ // { id, name, raw } or null if listing failed — caller falls back to a
185
+ // manual prompt.
186
+ async function listTrackerProjects({ tracker, creds, location }) {
187
+ if (tracker === "linear") {
188
+ if (!creds.LINEAR_API_KEY) return null;
189
+ try {
190
+ const res = await fetch("https://api.linear.app/graphql", {
191
+ method: "POST",
192
+ headers: { Authorization: creds.LINEAR_API_KEY, "Content-Type": "application/json" },
193
+ body: JSON.stringify({ query: "{ teams(first: 50) { nodes { id key name } } }" }),
194
+ signal: AbortSignal.timeout(8000),
195
+ });
196
+ if (!res.ok) return null;
197
+ const data = await res.json();
198
+ const nodes = data?.data?.teams?.nodes || [];
199
+ return nodes.map(t => ({ id: t.key, name: `${t.name} (${t.key})`, raw: t }));
200
+ } catch { return null; }
201
+ }
202
+ if (tracker === "jira") {
203
+ if (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN || !location) return null;
204
+ try {
205
+ const auth = "Basic " + Buffer.from(`${creds.JIRA_EMAIL}:${creds.JIRA_API_TOKEN}`).toString("base64");
206
+ const res = await fetch(`${location.replace(/\/+$/, "")}/rest/api/3/project/search?maxResults=50`, {
207
+ headers: { Authorization: auth, Accept: "application/json" },
208
+ signal: AbortSignal.timeout(8000),
209
+ });
210
+ if (!res.ok) return null;
211
+ const data = await res.json();
212
+ const values = data?.values || [];
213
+ return values.map(p => ({ id: p.key, name: `${p.name} (${p.key})`, raw: p }));
214
+ } catch { return null; }
215
+ }
216
+ if (tracker === "github") {
217
+ // GitHub has potentially thousands of repos; don't try to list — rely on
218
+ // the explicit repo prompt (which has a git-remote default anyway).
219
+ return null;
220
+ }
221
+ return null;
222
+ }
223
+
224
+ async function pickFromList(rl, prompt, items, { allowManual = true } = {}) {
225
+ const options = items.map(it => ({ label: it.name, value: it.id }));
226
+ if (allowManual) options.push({ label: "Type it manually instead", value: "__manual__" });
227
+ const choice = await selectOption(rl, prompt, options);
228
+ if (choice.value === "__manual__") return null;
229
+ return choice.value;
230
+ }
231
+
232
+ // Prompt for a value, showing instructions first, looping until non-empty.
233
+ async function promptRequired(rl, label, instructions) {
234
+ if (instructions) printSteps(instructions.title, instructions.steps);
235
+ while (true) {
236
+ const v = (await ask(rl, ` ${label}: `)).trim();
237
+ if (v) return v;
238
+ console.log(` ${label} is required. Ctrl+C to abort.`);
239
+ }
240
+ }
241
+
242
+ // Verify tracker auth with what we have in .env + server, then fetch
243
+ // available teams/projects and let the user pick from a list.
244
+ async function verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location }) {
245
+ console.log(" Verifying tracker access...");
246
+ const dotEnv = loadDotEnv(cwd);
247
+ const apiKey = loadApiKey(cwd);
248
+ const serverCreds = await fetchServerCreds(apiKey, finalProjectKey);
249
+ const credentials = resolveCredentialsFromEnv({ ...dotEnv, ...serverCreds });
250
+ const check = await checkTrackerPermissions({ tracker, config, credentials });
251
+ if (!check.ok) {
252
+ console.log(" ⚠ " + (check.errors[0] || "Verification failed"));
253
+ for (const e of check.errors.slice(1)) console.log(` • ${e}`);
254
+ console.log("");
255
+ return { ok: false };
256
+ }
257
+ console.log(" ✓ Tracker access verified");
258
+ printIdentityEcho(check.identity);
259
+ console.log("");
260
+
261
+ // GitHub Issues — project = repo, already collected above. Skip the pick.
262
+ if (tracker === "github") return { ok: true, trackerProjectId: null };
263
+
264
+ // Fetch selectable items (Linear teams / Jira projects) and let user pick.
265
+ const items = await listTrackerProjects({ tracker, creds: credentials, location });
266
+ if (!items || items.length === 0) {
267
+ console.log(" Couldn't list projects automatically — type the key manually.");
268
+ const label = tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)";
269
+ const manual = await promptRequired(rl, label);
270
+ console.log("");
271
+ return { ok: true, trackerProjectId: manual };
272
+ }
273
+ console.log("");
274
+ const picked = await pickFromList(rl, `Pick the ${tracker === "linear" ? "team" : "project"} this agentdesk project maps to:`, items);
275
+ console.log("");
276
+ if (picked) return { ok: true, trackerProjectId: picked };
277
+ const label = tracker === "linear" ? "Linear team key" : "Jira project key";
278
+ const manual = await promptRequired(rl, label);
279
+ console.log("");
280
+ return { ok: true, trackerProjectId: manual };
281
+ }
282
+
158
283
  export async function runInit(cwd, opts = {}) {
159
284
  const quick = !!opts.quick;
160
285
  const project = detectProject(cwd);
@@ -201,21 +326,14 @@ export async function runInit(cwd, opts = {}) {
201
326
  if (project.lintCommand) console.log(` Lint: ${project.lintCommand}`);
202
327
  if (project.testCommand || project.buildCommand || project.lintCommand) console.log("");
203
328
 
204
- // --- Step 1: Project key ---
205
- const defaultKey = existingConfig.projectKey || projectId;
206
- let finalProjectKey;
207
- if (editScope === "tracker") {
208
- finalProjectKey = defaultKey;
209
- } else {
210
- console.log(" Step 1 of 4 — Project key");
211
- console.log("");
212
- const keyAnswer = await ask(rl, ` Project key (${defaultKey}): `);
213
- finalProjectKey = keyAnswer.trim() || defaultKey;
214
- console.log("");
215
- }
329
+ // --- Tracker step: platform location → auth → verify → pick project ---
330
+ const config = {};
331
+ // Initial default for the agentdesk project key. Gets overwritten with the
332
+ // tracker's project/team key if we learn one during verification, and the
333
+ // user can override at the final review screen.
334
+ let finalProjectKey = existingConfig.projectKey || projectId;
216
335
 
217
- // --- Step 2: Tracker selection (platform first) ---
218
- if (!quick) console.log(editScope === "tracker" ? " Tracker setup" : " Step 2 of 4 — Task tracker");
336
+ if (!quick) console.log(editScope === "tracker" ? " Tracker" : " Step 1 of 3 — Task tracker");
219
337
  console.log("");
220
338
  const trackerOptions = [
221
339
  { label: "Linear", value: "linear" },
@@ -223,269 +341,144 @@ export async function runInit(cwd, opts = {}) {
223
341
  { label: "GitHub Issues", value: "github" },
224
342
  { label: "None (use descriptions only)", value: null },
225
343
  ];
226
-
227
- const selected = await selectOption(rl, "Task tracker:", trackerOptions);
228
- const tracker = selected.value;
344
+ const tracker = (await selectOption(rl, "Task tracker:", trackerOptions)).value;
229
345
  console.log("");
230
-
231
- // Inline guidance: dedicated-user vs personal, only when a tracker is picked.
232
- printDedicatedUserChoice(tracker, quick);
233
-
234
- // --- Step 2: Tracker configuration (details + credentials) ---
235
- const config = {};
236
346
  if (tracker) config.tracker = tracker;
237
347
 
348
+ if (tracker) printDedicatedUserChoice(tracker, quick);
349
+
350
+ // Tracker location + auth with exact instructions, then verify.
238
351
  if (tracker === "linear") {
239
- const currentWs = existingConfig.linear?.workspace || "";
240
- const wsAnswer = await ask(rl, ` Linear workspace slug${currentWs ? ` (${currentWs})` : ""}: `);
241
- const ws = wsAnswer.trim() || currentWs;
242
- const currentTeamKey = existingConfig.linear?.teamKey || "";
243
- const tkAnswer = await ask(rl, ` Linear team key${currentTeamKey ? ` (${currentTeamKey})` : ""} (e.g. KEN): `);
244
- const teamKey = tkAnswer.trim() || currentTeamKey;
245
- if (ws || teamKey) config.linear = { ...(ws && { workspace: ws }), ...(teamKey && { teamKey }) };
352
+ if (!quick) printSteps("Create a Linear API key", [
353
+ "Open https://linear.app/settings/api in your browser (log in first if needed).",
354
+ "Click \"Create key\" (top right of the API page).",
355
+ "Label the key: AgentDesk",
356
+ "Expiration: set a value matching your policy, or choose \"No expiration\".",
357
+ "Click \"Create\". Copy the key shown (starts with lin_api_). You will NOT see it again.",
358
+ ]);
359
+ const ws = await promptRequired(rl, "Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
360
+ const key = await promptRequired(rl, "Linear API key you just copied");
361
+ saveEnvVar(cwd, "LINEAR_API_KEY", key);
362
+ console.log(" ✓ Saved LINEAR_API_KEY to .env");
246
363
  console.log("");
364
+ config.linear = { workspace: ws };
247
365
  }
248
366
 
249
367
  if (tracker === "jira") {
250
- const currentUrl = existingConfig.jira?.baseUrl || "";
251
- const urlAnswer = await ask(rl, ` Jira base URL${currentUrl ? ` (${currentUrl})` : ""}: `);
252
- const url = urlAnswer.trim() || currentUrl;
253
- const currentProj = existingConfig.jira?.project || "";
254
- const projAnswer = await ask(rl, ` Jira project key${currentProj ? ` (${currentProj})` : ""} (e.g. PROJ): `);
255
- const proj = projAnswer.trim() || currentProj;
256
- if (url || proj) config.jira = { ...(url && { baseUrl: url }), ...(proj && { project: proj }) };
368
+ if (!quick) printSteps("Create a Jira API token", [
369
+ "Open https://id.atlassian.com/manage-profile/security/api-tokens",
370
+ "Click \"Create API token\".",
371
+ "Label: AgentDesk",
372
+ "Click \"Create\". Copy the token (shown once).",
373
+ "You'll also need the email you sign into Atlassian with.",
374
+ ]);
375
+ const baseUrl = await promptRequired(rl, "Jira tenant URL (e.g. https://yourco.atlassian.net)");
376
+ const email = await promptRequired(rl, "Your Atlassian login email");
377
+ const token = await promptRequired(rl, "Jira API token you just created");
378
+ saveEnvVar(cwd, "JIRA_EMAIL", email);
379
+ saveEnvVar(cwd, "JIRA_API_TOKEN", token);
380
+ console.log(" ✓ Saved JIRA_EMAIL and JIRA_API_TOKEN to .env");
257
381
  console.log("");
382
+ config.jira = { baseUrl: baseUrl.replace(/\/+$/, "") };
258
383
  }
259
384
 
260
- let skipGithubVerify = false;
261
385
  if (tracker === "github") {
262
- const current = existingConfig.github?.repo || "";
263
- const answer = await ask(rl, ` GitHub repo${current ? ` (${current})` : ""} (owner/repo): `);
264
- const r = answer.trim() || current;
265
- if (r) config.github = { repo: r };
266
- console.log("");
267
-
268
- // Ask which identity to use for this project, regardless of whether
269
- // `gh auth` is already set up globally. Without this prompt the wizard
270
- // silently reuses whichever account the user last ran `gh auth login`
271
- // with, which is surprising on a machine with multiple GitHub identities.
272
- let ghLogin = null;
273
- try {
274
- const out = execSync("gh api user --jq .login", { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
275
- if (out) ghLogin = out;
276
- } catch {}
277
- const ghOptionLabel = ghLogin
278
- ? `Use gh CLI — currently authenticated as @${ghLogin}`
279
- : `Use gh CLI — you'll run 'gh auth login' after this wizard`;
280
- const authChoice = await selectOption(rl, "GitHub authentication for this project:", [
281
- { label: ghOptionLabel, value: "gh" },
282
- { label: "Paste a personal access token (use a different account)", value: "pat" },
283
- { label: "Skip — configure later in the dashboard", value: "skip" },
386
+ // The GitHub-issues credential doubles as the git-push credential below,
387
+ // so we collect a full repo/login/token block up front.
388
+ if (!quick) printSteps("Create a GitHub personal access token", [
389
+ "Open https://github.com/settings/tokens (classic tokens).",
390
+ "Click \"Generate new token\" → \"Generate new token (classic)\".",
391
+ "Name: AgentDesk",
392
+ "Expiration: match your policy.",
393
+ "Scopes: check \"repo\" (full control of private repositories).",
394
+ "Click \"Generate token\" at the bottom. Copy the token (shown once).",
284
395
  ]);
396
+ const detected = detectGitRemote(cwd);
397
+ const repoDefault = existingConfig.github?.repo || detected || "";
398
+ const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
399
+ const loginDefault = existingConfig.github?.login || "";
400
+ const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
401
+ const token = await promptRequired(rl, "GitHub token");
402
+ saveEnvVar(cwd, "GITHUB_TOKEN", token);
403
+ console.log(" ✓ Saved GITHUB_TOKEN to .env");
285
404
  console.log("");
405
+ config.github = { repo, login };
406
+ }
286
407
 
287
- if (authChoice.value === "skip") {
288
- skipGithubVerify = true;
289
- console.log(" Skipping verification. Configure GitHub auth later via the dashboard or re-run `agentdesk init`.");
408
+ // Verify tracker + fetch available projects to pick from.
409
+ let trackerLocation = null;
410
+ if (tracker === "jira") trackerLocation = config.jira?.baseUrl;
411
+ if (tracker) {
412
+ const verified = await verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location: trackerLocation });
413
+ if (!verified.ok) {
414
+ console.log(" Continuing without tracker verification.");
415
+ console.log(" You can re-run `agentdesk init` after fixing credentials.");
290
416
  console.log("");
291
- } else if (authChoice.value === "pat") {
292
- if (!quick) {
293
- console.log(" Create a token:");
294
- console.log(" 1. Open https://github.com/settings/tokens");
295
- console.log(" 2. Click \"Generate new token (classic)\"");
296
- console.log(" 3. Name: AgentDesk. Scope: repo. Click Generate.");
297
- console.log(" 4. Copy the token and paste below.");
298
- console.log("");
299
- }
300
- while (true) {
301
- const tokenInput = (await ask(rl, " GitHub token: ")).trim();
302
- if (!tokenInput) {
303
- console.log(" Token is required. Press Ctrl+C to abort.");
304
- continue;
305
- }
306
- saveEnvVar(cwd, "GITHUB_TOKEN", tokenInput);
307
- console.log(" ✓ Saved to .env (as GITHUB_TOKEN). This overrides any global gh CLI auth for this project.");
308
- console.log("");
309
- break;
310
- }
417
+ } else if (verified.trackerProjectId) {
418
+ if (tracker === "linear") config.linear = { ...(config.linear || {}), teamKey: verified.trackerProjectId };
419
+ if (tracker === "jira") config.jira = { ...(config.jira || {}), project: verified.trackerProjectId };
420
+ // Use the tracker's project key as the default agentdesk project key.
421
+ if (!existingConfig.projectKey) finalProjectKey = verified.trackerProjectId.toLowerCase();
311
422
  }
312
423
  }
313
424
 
314
- // --- Step 3: Verify tracker permissions (with re-prompt on failure) ---
315
- if (tracker && !skipGithubVerify) {
316
- let verified = false;
317
- while (!verified) {
318
- console.log(" Checking tracker permissions...");
319
- const dotEnv = loadDotEnv(cwd);
320
- const apiKey = loadApiKey(cwd);
321
- const serverCreds = await fetchServerCreds(apiKey, finalProjectKey);
322
- const credentials = resolveCredentialsFromEnv({ ...dotEnv, ...serverCreds });
323
- const check = await checkTrackerPermissions({ tracker, config, credentials });
324
-
325
- if (check.ok) {
326
- console.log(" Tracker permissions verified (read, create, update)");
327
- printIdentityEcho(check.identity);
328
- console.log("");
329
- verified = true;
330
- } else {
331
- console.log("");
332
- console.log(" ⚠ Tracker permission issues:");
333
- for (const err of check.errors) {
334
- console.log(` • ${err}`);
335
- }
336
- console.log("");
337
-
338
- // Determine what's missing and offer to fix it
339
- const missingCreds = check.errors.some(e =>
340
- e.includes("Missing") && (e.includes("API_KEY") || e.includes("TOKEN") || e.includes("EMAIL"))
341
- );
342
-
343
- if (missingCreds) {
344
- console.log(" Let's fix this now. I'll walk you through it.");
345
- console.log("");
346
-
347
- if (tracker === "linear") {
348
- console.log(" ┌─────────────────────────────────────────────┐");
349
- console.log(" │ How to get your Linear API key: │");
350
- console.log(" │ │");
351
- console.log(" │ 1. Open https://linear.app/settings/api │");
352
- console.log(" │ 2. Click \"Create new API key\" │");
353
- console.log(" │ 3. Give it a label (e.g. \"AgentDesk\") │");
354
- console.log(" │ 4. Copy the key │");
355
- console.log(" │ 5. Paste it below │");
356
- console.log(" └─────────────────────────────────────────────┘");
357
- console.log("");
358
-
359
- while (true) {
360
- const keyInput = await ask(rl, " Linear API key: ");
361
- const key = keyInput.trim();
362
- if (!key) {
363
- console.log(" The API key is required for the agents to read and update your Linear tasks.");
364
- console.log("");
365
- continue;
366
- }
367
- saveEnvVar(cwd, "LINEAR_API_KEY", key);
368
- console.log(" ✓ Saved to .env");
369
- console.log("");
370
- break;
371
- }
372
- continue; // Re-check with the new key
373
- }
374
-
375
- if (tracker === "jira") {
376
- console.log(" ┌──────────────────────────────────────────────────────────┐");
377
- console.log(" │ How to get your Jira API token: │");
378
- console.log(" │ │");
379
- console.log(" │ 1. Open https://id.atlassian.com/manage-profile/ │");
380
- console.log(" │ security/api-tokens │");
381
- console.log(" │ 2. Click \"Create API token\" │");
382
- console.log(" │ 3. Give it a label (e.g. \"AgentDesk\") │");
383
- console.log(" │ 4. Copy the token │");
384
- console.log(" │ │");
385
- console.log(" │ You'll also need the email address you use │");
386
- console.log(" │ to log into Jira (not your username). │");
387
- console.log(" └──────────────────────────────────────────────────────────┘");
388
- console.log("");
389
-
390
- if (!credentials.JIRA_EMAIL) {
391
- while (true) {
392
- const emailInput = (await ask(rl, " Your Jira email address: ")).trim();
393
- if (!emailInput || !emailInput.includes("@")) {
394
- console.log(" Please enter a valid email address (the one you use to log into Jira).");
395
- continue;
396
- }
397
- saveEnvVar(cwd, "JIRA_EMAIL", emailInput);
398
- console.log(" ✓ Email saved");
399
- break;
400
- }
401
- }
402
-
403
- while (true) {
404
- const tokenInput = (await ask(rl, " Jira API token: ")).trim();
405
- if (!tokenInput) {
406
- console.log(" The API token is required for the agents to read and update your Jira tasks.");
407
- console.log("");
408
- continue;
409
- }
410
- saveEnvVar(cwd, "JIRA_API_TOKEN", tokenInput);
411
- console.log(" ✓ Token saved to .env");
412
- console.log("");
413
- break;
414
- }
415
- continue; // Re-check
416
- }
417
-
418
- if (tracker === "github") {
419
- console.log(" ┌──────────────────────────────────────────────────────────┐");
420
- console.log(" │ Two ways to authenticate with GitHub: │");
421
- console.log(" │ │");
422
- console.log(" │ Option A — GitHub CLI (recommended): │");
423
- console.log(" │ Run: gh auth login │");
424
- console.log(" │ Then press Enter below. │");
425
- console.log(" │ │");
426
- console.log(" │ Option B — Personal Access Token: │");
427
- console.log(" │ 1. Open https://github.com/settings/tokens │");
428
- console.log(" │ 2. Click \"Generate new token (classic)\" │");
429
- console.log(" │ 3. Select the \"repo\" scope │");
430
- console.log(" │ 4. Generate and copy the token │");
431
- console.log(" │ 5. Paste it below │");
432
- console.log(" └──────────────────────────────────────────────────────────┘");
433
- console.log("");
434
-
435
- const tokenInput = await ask(rl, " GitHub token (or Enter if using gh CLI): ");
436
- const token = tokenInput.trim();
437
- if (token) {
438
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
439
- console.log(" ✓ Token saved to .env");
440
- console.log("");
441
- } else {
442
- console.log(" OK, will check gh CLI authentication...");
443
- console.log("");
444
- }
445
- continue; // Re-check
446
- }
447
- }
448
-
449
- // Non-credential error (wrong team key, wrong URL, network issue, etc.)
450
- console.log("");
451
- const choice = await selectOption(rl, "What would you like to do?", [
452
- { label: "Fix it and try again", value: "retry" },
453
- { label: "Continue without tracker verification (fix later in dashboard)", value: "skip" },
454
- { label: "Choose a different tracker platform", value: "change" },
455
- ]);
456
-
457
- if (choice.value === "retry") {
458
- // Re-prompt for the tracker details that might be wrong
459
- if (tracker === "linear" && config.linear) {
460
- const wsAnswer = await ask(rl, ` Linear workspace slug (${config.linear.workspace || ""}): `);
461
- if (wsAnswer.trim()) config.linear.workspace = wsAnswer.trim();
462
- const tkAnswer = await ask(rl, ` Linear team key (${config.linear.teamKey || ""}): `);
463
- if (tkAnswer.trim()) config.linear.teamKey = tkAnswer.trim();
464
- }
465
- if (tracker === "jira" && config.jira) {
466
- const urlAnswer = await ask(rl, ` Jira base URL (${config.jira.baseUrl || ""}): `);
467
- if (urlAnswer.trim()) config.jira.baseUrl = urlAnswer.trim();
468
- const projAnswer = await ask(rl, ` Jira project key (${config.jira.project || ""}): `);
469
- if (projAnswer.trim()) config.jira.project = projAnswer.trim();
470
- }
471
- if (tracker === "github" && config.github) {
472
- const repoAnswer = await ask(rl, ` GitHub repo (${config.github.repo || ""}): `);
473
- if (repoAnswer.trim()) config.github.repo = repoAnswer.trim();
474
- }
475
- console.log("");
476
- continue;
477
- } else if (choice.value === "change") {
478
- rl.close();
479
- return runInit(cwd);
480
- } else {
481
- console.log("");
482
- console.log(" Continuing without tracker verification.");
483
- console.log(" You can configure credentials later at agentdesk.live or re-run 'agentdesk init'.");
484
- console.log("");
485
- verified = true;
486
- }
425
+ // --- GitHub block (always even when tracker is Linear/Jira/None) ---
426
+ // This configures the credential agents use for git push, PRs, and
427
+ // GitHub API calls. When tracker is already GitHub Issues the block
428
+ // above already collected repo+login+token, so skip the duplicate.
429
+ let githubLogin = config.github?.login || null;
430
+ if (tracker !== "github") {
431
+ if (!quick) {
432
+ console.log(" ─".repeat(30));
433
+ console.log(" GitHub access (required agents push code and open PRs here)");
434
+ console.log("");
435
+ }
436
+ if (!quick) printSteps("Create a GitHub personal access token", [
437
+ "Open https://github.com/settings/tokens (classic tokens).",
438
+ "Click \"Generate new token\" → \"Generate new token (classic)\".",
439
+ "Name: AgentDesk",
440
+ "Expiration: match your policy.",
441
+ "Scopes: check \"repo\" (full control of private repositories).",
442
+ "Click \"Generate token\" at the bottom. Copy the token (shown once).",
443
+ ]);
444
+ const detected = detectGitRemote(cwd);
445
+ const repoDefault = existingConfig.github?.repo || detected || "";
446
+ const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
447
+ const loginDefault = existingConfig.github?.login || "";
448
+ const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
449
+ const token = await promptRequired(rl, "GitHub token");
450
+ saveEnvVar(cwd, "GITHUB_TOKEN", token);
451
+ console.log(" Saved GITHUB_TOKEN to .env");
452
+ console.log("");
453
+ config.github = { repo, login };
454
+ githubLogin = login;
455
+
456
+ // Verify token matches the user-stated login.
457
+ try {
458
+ const env = { ...process.env, GH_TOKEN: token };
459
+ const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
460
+ if (who && login && who.toLowerCase() !== login.toLowerCase()) {
461
+ console.log(` ⚠ Token is valid but authenticates as @${who}, not @${login}. Agents will post as @${who}.`);
462
+ } else if (who) {
463
+ console.log(` GitHub verified as @${who}`);
487
464
  }
465
+ } catch {
466
+ console.log(" ⚠ Could not verify the GitHub token (gh CLI missing or network unreachable). Saved anyway — verify later in the dashboard.");
488
467
  }
468
+ console.log("");
469
+ }
470
+
471
+ // Identity badge — always ask (optional).
472
+ const badgeDefault = existingConfig.identityBadge || "";
473
+ const badge = (await ask(rl, ` Identity badge (display name for AgentDesk)${badgeDefault ? ` [${badgeDefault}]` : ""} [optional]: `)).trim() || badgeDefault;
474
+ if (badge) config.identityBadge = badge;
475
+ console.log("");
476
+
477
+ // --- Final step: agentdesk project key (with the tracker-derived default) ---
478
+ if (!quick) {
479
+ const keyAnswer = await ask(rl, ` AgentDesk project key [${finalProjectKey}]: `);
480
+ if (keyAnswer.trim()) finalProjectKey = keyAnswer.trim();
481
+ console.log("");
489
482
  }
490
483
 
491
484
  // --- Summary + confirm before write ---
@@ -496,26 +489,22 @@ export async function runInit(cwd, opts = {}) {
496
489
 
497
490
  merged.projectKey = finalProjectKey;
498
491
 
499
- if (tracker) {
500
- merged.tracker = tracker;
501
- if (config.linear) merged.linear = config.linear;
502
- if (config.jira) merged.jira = config.jira;
503
- if (config.github) merged.github = config.github;
504
- } else {
505
- delete merged.tracker;
506
- delete merged.linear;
507
- delete merged.jira;
508
- delete merged.github;
509
- }
492
+ if (tracker) merged.tracker = tracker;
493
+ else delete merged.tracker;
494
+ if (config.linear) merged.linear = config.linear; else if (!tracker) delete merged.linear;
495
+ if (config.jira) merged.jira = config.jira; else if (!tracker) delete merged.jira;
496
+ if (config.github) merged.github = config.github;
497
+ if (config.identityBadge) merged.identityBadge = config.identityBadge;
510
498
 
511
499
  if (!quick) {
512
500
  console.log(" Review");
513
501
  console.log(" ──────");
514
- console.log(` Project key: ${merged.projectKey}`);
515
- console.log(` Tracker: ${merged.tracker || "none"}`);
516
- if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
517
- if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
518
- if (merged.github) console.log(` GitHub: ${merged.github.repo || "-"}`);
502
+ console.log(` Project key: ${merged.projectKey}`);
503
+ console.log(` Tracker: ${merged.tracker || "none"}`);
504
+ if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
505
+ if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
506
+ if (merged.github) console.log(` GitHub: repo=${merged.github.repo || "-"}, user=@${merged.github.login || "-"}`);
507
+ if (merged.identityBadge) console.log(` Identity badge: ${merged.identityBadge}`);
519
508
  console.log("");
520
509
  const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
521
510
  if (confirm && confirm !== "y" && confirm !== "yes") {
@@ -94,9 +94,13 @@ async function checkJira({ baseUrl, project }, creds) {
94
94
  let identity = null;
95
95
  const auth = "Basic " + Buffer.from(`${email}:${token}`).toString("base64");
96
96
 
97
+ // Normalize the base URL: trim trailing slashes, so `...net/` doesn't
98
+ // produce `...net//rest/...` which some Atlassian paths reject.
99
+ const cleanBase = String(baseUrl || "").replace(/\/+$/, "");
100
+
97
101
  // Fetch the authenticated user so we can report "Posting as X".
98
102
  try {
99
- const meRes = await fetch(`${baseUrl}/rest/api/3/myself`, {
103
+ const meRes = await fetch(`${cleanBase}/rest/api/3/myself`, {
100
104
  headers: { Authorization: auth, Accept: "application/json" },
101
105
  signal: AbortSignal.timeout(10000),
102
106
  });
@@ -106,15 +110,24 @@ async function checkJira({ baseUrl, project }, creds) {
106
110
  }
107
111
  } catch {}
108
112
 
109
- // Use Jira's mypermissions endpoint to check all permissions at once
113
+ // Use Jira's mypermissions endpoint to check all permissions at once.
114
+ // Try REST v3 first (Jira Cloud), fall back to v2 on 404 (older tenants /
115
+ // Data Center installs where v3 isn't exposed).
110
116
  const permissionsToCheck = "BROWSE_PROJECTS,CREATE_ISSUES,EDIT_ISSUES,ADD_COMMENTS,TRANSITION_ISSUES";
117
+ const tryVersions = ["3", "2"];
118
+ let res = null;
119
+ let lastUrl = "";
111
120
  try {
112
- const url = `${baseUrl}/rest/api/3/mypermissions?permissions=${permissionsToCheck}` +
113
- (project ? `&projectKey=${project}` : "");
114
- const res = await fetch(url, {
115
- headers: { Authorization: auth, Accept: "application/json" },
116
- signal: AbortSignal.timeout(10000),
117
- });
121
+ for (const v of tryVersions) {
122
+ const url = `${cleanBase}/rest/api/${v}/mypermissions?permissions=${permissionsToCheck}` +
123
+ (project ? `&projectKey=${project}` : "");
124
+ lastUrl = url;
125
+ res = await fetch(url, {
126
+ headers: { Authorization: auth, Accept: "application/json" },
127
+ signal: AbortSignal.timeout(10000),
128
+ });
129
+ if (res.status !== 404) break;
130
+ }
118
131
 
119
132
  if (res.status === 401) {
120
133
  return { ok: false, errors: ["Jira authentication failed — check JIRA_EMAIL and JIRA_API_TOKEN"] };
@@ -123,7 +136,7 @@ async function checkJira({ baseUrl, project }, creds) {
123
136
  return { ok: false, errors: ["Jira access forbidden — your account may lack access to this project"] };
124
137
  }
125
138
  if (!res.ok) {
126
- return { ok: false, errors: [`Jira API error (${res.status}) — check your base URL: ${baseUrl}`] };
139
+ return { ok: false, errors: [`Jira API error (${res.status}) calling ${lastUrl} — check that the base URL is the Atlassian tenant root (e.g. https://yourco.atlassian.net) without a path.`] };
127
140
  }
128
141
 
129
142
  const data = await res.json();
@@ -144,9 +157,9 @@ async function checkJira({ baseUrl, project }, creds) {
144
157
  }
145
158
  } catch (e) {
146
159
  if (e.name === "AbortError" || e.name === "TimeoutError") {
147
- return { ok: false, errors: [`Cannot reach Jira at ${baseUrl} — request timed out`] };
160
+ return { ok: false, errors: [`Cannot reach Jira at ${cleanBase} — request timed out`] };
148
161
  }
149
- return { ok: false, errors: [`Cannot reach Jira at ${baseUrl}: ${e.message}`] };
162
+ return { ok: false, errors: [`Cannot reach Jira at ${cleanBase}: ${e.message}`] };
150
163
  }
151
164
 
152
165
  return { ok: errors.length === 0, errors, identity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.17.6",
3
+ "version": "0.18.1",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {