@kendoo.agentdesk/agentdesk 0.9.5 → 0.9.7

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.
@@ -1,18 +1,15 @@
1
- // Orchestrator — manages agent sub-sessions across phases
1
+ // Orchestrator — runs a single Claude process with all agents as personas
2
2
 
3
3
  import { spawn } from "child_process";
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { createInterface } from "readline";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
- import { runAgent } from "./agent-runner.mjs";
9
8
  import { buildPrompt } from "./prompt.mjs";
10
9
  import { createStreamParser } from "./stream-parser.mjs";
11
10
 
12
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
12
  const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
14
- import { buildAgentPrompt, getAgentTools } from "./agent-prompts.mjs";
15
- import { detectProject, generateContext } from "./detect.mjs";
16
13
 
17
14
  // Fetch decrypted tracker credentials from server
18
15
  async function fetchTrackerCredentials(projectName, apiKey, serverUrl) {
@@ -50,274 +47,8 @@ function timestamp() {
50
47
  export async function runOrchestrator({
51
48
  taskId, taskLink, description, createTask, tracker, config,
52
49
  project, team, teamSections, inboxUrl, sessionUrl, cwd,
53
- onEvent, mode = "full", apiKey, serverUrl,
50
+ onEvent, apiKey, serverUrl,
54
51
  }) {
55
- // --- Lite mode: single process, legacy behavior ---
56
- if (mode === "lite") {
57
- return runLiteMode({ taskId, taskLink, description, createTask, tracker, config, project, teamSections, inboxUrl, sessionUrl, cwd, onEvent, apiKey, serverUrl });
58
- }
59
-
60
- // --- Full mode: independent sub-agents ---
61
- const state = {
62
- task: { id: taskId, link: taskLink, description: description || "" },
63
- conversationLog: [], // { type: "message"|"phase", agent?, tag?, message?, phase? }
64
- phaseSummaries: {},
65
- totalInputTokens: 0,
66
- totalOutputTokens: 0,
67
- totalSteps: 0,
68
- };
69
-
70
- const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
71
- const env = { ...process.env, ...loadDotEnv(cwd), ...trackerCreds };
72
-
73
- // Emit event to WebSocket (same protocol as single-process mode)
74
- function emit(event) {
75
- onEvent?.({ ...event, timestamp: timestamp() });
76
- }
77
-
78
- // Add to conversation log
79
- function logMessage(agent, tag, message) {
80
- state.conversationLog.push({ type: "message", agent, tag, message });
81
- }
82
-
83
- function logPhase(phase) {
84
- state.conversationLog.push({ type: "phase", phase });
85
- }
86
-
87
- // Run a single agent and collect results
88
- async function spawnAgent(agentName, agent, phase, extraConversation) {
89
- const conversation = extraConversation || state.conversationLog;
90
-
91
- const prompt = buildAgentPrompt(agent, {
92
- phase,
93
- task: state.task,
94
- project,
95
- conversation,
96
- inboxUrl,
97
- sessionUrl,
98
- });
99
-
100
- const tools = getAgentTools(agentName);
101
-
102
- const result = await runAgent(agentName, {
103
- prompt,
104
- allowedTools: tools,
105
- cwd,
106
- env,
107
- onMessage(msg) {
108
- emit({ type: "agent:message", agent: msg.agent, tag: msg.tag, message: msg.message });
109
- logMessage(msg.agent, msg.tag, msg.message);
110
- },
111
- onToolUse(use) {
112
- emit({ type: "tool:use", agent: use.agent, tool: use.tool, description: use.description });
113
- },
114
- onToolResult(res) {
115
- emit({ type: "tool:result", success: res.success, summary: res.summary });
116
- },
117
- });
118
-
119
- state.totalInputTokens += result.inputTokens;
120
- state.totalOutputTokens += result.outputTokens;
121
- state.totalSteps += result.steps;
122
-
123
- return result;
124
- }
125
-
126
- // Run multiple agents in parallel
127
- async function spawnAgentsParallel(agents, phase, extraConversation) {
128
- const conversation = extraConversation || [...state.conversationLog];
129
- return Promise.all(
130
- agents.map(([name, agent]) => spawnAgent(name, agent, phase, conversation))
131
- );
132
- }
133
-
134
- // Build summary from agent results
135
- function summarizeResults(results) {
136
- return results
137
- .filter(r => r.messages.length > 0)
138
- .map(r => r.messages.map(m => `${m.agent} [${m.tag}]: ${m.message}`).join("\n"))
139
- .join("\n\n");
140
- }
141
-
142
- // Detect if any agent argued
143
- function hasDisagreement(results) {
144
- return results.some(r => r.hadArgue);
145
- }
146
-
147
- // --- Session start ---
148
- const startTime = Date.now();
149
- emit({
150
- type: "session:start",
151
- taskId, taskLink,
152
- title: description || taskId,
153
- project: project?.name || null,
154
- sessionNumber: 1,
155
- agents: teamSections.names,
156
- cliVersion: CLI_VERSION,
157
- });
158
-
159
- // Build agent lookup
160
- const agentMap = new Map();
161
- for (const agent of team) {
162
- agentMap.set(agent.name, agent);
163
- }
164
-
165
- // Find specific agents
166
- const jane = agentMap.get("Jane");
167
- const dennis = agentMap.get("Dennis");
168
-
169
- // ===========================
170
- // PHASE 1: INTAKE (Jane only)
171
- // ===========================
172
- emit({ type: "phase:change", phase: "INTAKE" });
173
- logPhase("INTAKE");
174
-
175
- const intakeResult = await spawnAgent("Jane", jane, "intake");
176
- state.phaseSummaries.intake = summarizeResults([intakeResult]);
177
-
178
- // Extract short title from Jane's intake (SESSION_TITLE: ...)
179
- const titleMatch = intakeResult.rawOutput.match(/SESSION_TITLE:\s*(.+)/);
180
- if (titleMatch) {
181
- const shortTitle = titleMatch[1].trim().slice(0, 60);
182
- emit({ type: "session:update", title: shortTitle });
183
- }
184
-
185
- // ===========================
186
- // PHASE 2: BRAINSTORM (parallel, 3-5 rounds)
187
- // ===========================
188
- emit({ type: "phase:change", phase: "BRAINSTORM" });
189
- logPhase("BRAINSTORM");
190
-
191
- const allAgents = [...agentMap.entries()];
192
- let brainstormRounds = 0;
193
- const MAX_BRAINSTORM_ROUNDS = 5;
194
-
195
- for (let round = 0; round < MAX_BRAINSTORM_ROUNDS; round++) {
196
- brainstormRounds++;
197
- const results = await spawnAgentsParallel(allAgents, "brainstorm");
198
-
199
- // Check for consensus (no ARGUE tags = consensus)
200
- if (!hasDisagreement(results) && round >= 2) {
201
- break; // Minimum 3 rounds, then stop on consensus
202
- }
203
- }
204
- state.phaseSummaries.brainstorm = `Brainstorm completed in ${brainstormRounds} rounds.`;
205
-
206
- // ===========================
207
- // PHASE 3: PLANNING (parallel + review)
208
- // ===========================
209
- emit({ type: "phase:change", phase: "PLANNING" });
210
- logPhase("PLANNING");
211
-
212
- // Round 1: each agent presents their plan
213
- await spawnAgentsParallel(allAgents, "planning");
214
-
215
- // Round 2: review all plans, raise objections
216
- const planReviewResults = await spawnAgentsParallel(allAgents, "planning-review");
217
- state.phaseSummaries.planning = "Plans finalized.";
218
-
219
- // If there were objections, one more planning round
220
- if (hasDisagreement(planReviewResults)) {
221
- await spawnAgentsParallel(allAgents, "planning");
222
- }
223
-
224
- // ===========================
225
- // PHASE 4: EXECUTION (sequential)
226
- // ===========================
227
- emit({ type: "phase:change", phase: "EXECUTION" });
228
- logPhase("EXECUTION");
229
-
230
- // Step 1: Dennis implements
231
- if (dennis) {
232
- const dennisResult = await spawnAgent("Dennis", dennis, "execution");
233
-
234
- // Step 2: Sam audits (if on team)
235
- const sam = agentMap.get("Sam");
236
- if (sam) {
237
- let fixAttempts = 0;
238
- const MAX_FIX_ATTEMPTS = 3;
239
-
240
- while (fixAttempts < MAX_FIX_ATTEMPTS) {
241
- const samResult = await spawnAgent("Sam", sam, "execution-review");
242
-
243
- if (!samResult.hadArgue) break; // No issues found
244
-
245
- // Dennis fixes Sam's issues
246
- fixAttempts++;
247
- if (fixAttempts < MAX_FIX_ATTEMPTS) {
248
- await spawnAgent("Dennis", dennis, "execution");
249
- }
250
- }
251
- }
252
-
253
- // Step 3: Luna + Mark review in parallel (if on team)
254
- const reviewAgents = [];
255
- const luna = agentMap.get("Luna");
256
- const mark = agentMap.get("Mark");
257
- if (luna) reviewAgents.push(["Luna", luna]);
258
- if (mark) reviewAgents.push(["Mark", mark]);
259
-
260
- if (reviewAgents.length > 0) {
261
- const reviewResults = await spawnAgentsParallel(reviewAgents, "execution-review");
262
-
263
- // If Luna or Mark found issues, Dennis fixes
264
- if (hasDisagreement(reviewResults)) {
265
- await spawnAgent("Dennis", dennis, "execution");
266
- }
267
- }
268
-
269
- // Step 4: Vera writes tests (if on team)
270
- const vera = agentMap.get("Vera");
271
- if (vera) {
272
- await spawnAgent("Vera", vera, "execution");
273
- }
274
-
275
- // Step 5: Bart does QA (if on team)
276
- const bart = agentMap.get("Bart");
277
- if (bart) {
278
- await spawnAgent("Bart", bart, "execution");
279
- }
280
- }
281
-
282
- state.phaseSummaries.execution = "Execution complete.";
283
-
284
- // ===========================
285
- // PHASE 5: REVIEW (Jane + parallel)
286
- // ===========================
287
- emit({ type: "phase:change", phase: "REVIEW" });
288
- logPhase("REVIEW");
289
-
290
- // All agents provide final notes in parallel
291
- await spawnAgentsParallel(allAgents, "review");
292
-
293
- // Jane wraps up
294
- if (jane) {
295
- await spawnAgent("Jane", jane, "review");
296
- }
297
-
298
- // ===========================
299
- // SESSION END
300
- // ===========================
301
- const duration = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
302
- emit({
303
- type: "session:end",
304
- duration,
305
- steps: state.totalSteps,
306
- inputTokens: state.totalInputTokens,
307
- outputTokens: state.totalOutputTokens,
308
- });
309
-
310
- return {
311
- duration,
312
- steps: state.totalSteps,
313
- inputTokens: state.totalInputTokens,
314
- outputTokens: state.totalOutputTokens,
315
- };
316
- }
317
-
318
- // --- Lite mode: single Claude process with all agents as personas ---
319
-
320
- async function runLiteMode({ taskId, taskLink, description, createTask, tracker, config, project, teamSections, inboxUrl, sessionUrl, cwd, onEvent, apiKey, serverUrl }) {
321
52
  const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
322
53
  const env = { ...process.env, ...loadDotEnv(cwd), ...trackerCreds };
323
54
  const startTime = Date.now();
package/cli/prompt.mjs CHANGED
@@ -17,7 +17,6 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
17
17
  prompt = prompt.replace(/\{\{SPEAKING_ORDER\}\}/g, teamSections.speakingOrder);
18
18
  prompt = prompt.replace(/\{\{GROUND_RULES\}\}/g, teamSections.groundRules);
19
19
  prompt = prompt.replace(/\{\{CODE_PRINCIPLES\}\}/g, teamSections.codePrinciples);
20
- prompt = prompt.replace(/\{\{BRAINSTORM_ORDER\}\}/g, teamSections.brainstormOrder);
21
20
  prompt = prompt.replace(/\{\{PLANNING_ORDER\}\}/g, teamSections.planningOrder);
22
21
  prompt = prompt.replace(/\{\{EXECUTION_STEPS\}\}/g, teamSections.executionSteps);
23
22
 
@@ -35,7 +34,7 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
35
34
 
36
35
  // Create task instruction
37
36
  if (createTask && description) {
38
- let createInstr = "\n\n## CREATE TASK\n\nNo task ID was provided. Before starting work, Jane MUST create a new task in the tracker:\n\n";
37
+ let createInstr = "\n\n## CREATE TASK (MANDATORY — FIRST ACTION IN INTAKE)\n\nNo task ID was provided only a description. Jane MUST create a new task in the tracker as the VERY FIRST action before anything else.\n\n";
39
38
  if (tracker === "linear") {
40
39
  createInstr += `Create a Linear issue using the GraphQL API:\n- Endpoint: https://api.linear.app/graphql\n- Auth: Authorization: $LINEAR_API_KEY\n- Set the title based on the description below\n- After creation, use the returned identifier (e.g., KEN-530) as the task ID for the rest of the session\n`;
41
40
  } else if (tracker === "jira") {
@@ -44,7 +43,12 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
44
43
  createInstr += `Create a GitHub issue:\n- Run: gh issue create --title "..." --body "..."\n- After creation, use the returned issue number as the task ID for the rest of the session\n`;
45
44
  }
46
45
  createInstr += `\nTask description: ${description}\n`;
47
- createInstr += `\nIMPORTANT: After creating the task, Jane MUST immediately announce the new task ID on its own line in this exact format:\nTASK_ID: <identifier>\nExample: TASK_ID: KEN-530\nThis is required so the session can be linked to the correct task.\n`;
46
+ createInstr += `\nAfter creating the task, Jane MUST:\n`;
47
+ createInstr += `1. ANNOUNCE the new task ID clearly to the team: "I've created task <ID> in ${tracker}. This is our task for this session."\n`;
48
+ createInstr += `2. Output the task ID on its own line in this exact format (required for dashboard linking):\n TASK_ID: <identifier>\n Example: TASK_ID: KEN-530\n`;
49
+ createInstr += `3. Immediately set the task status to "In Progress" and post the session start comment.\n`;
50
+ createInstr += `4. Use this new task ID for ALL subsequent tracker operations (comments, status updates, PR linking).\n`;
51
+ createInstr += `\nDo NOT proceed to PLAN until the task is created, announced, and set to "In Progress".\n`;
48
52
  prompt += createInstr;
49
53
  }
50
54
 
@@ -1,6 +1,6 @@
1
1
  // Shared Claude stream-json parser — used by both `agentdesk team` and `agentdesk daemon`
2
2
 
3
- const PHASE_NAMES = ["INTAKE", "BRAINSTORM", "PLANNING", "EXECUTION", "REVIEW"];
3
+ const PHASE_NAMES = ["INTAKE", "PLAN", "EXECUTION", "REVIEW", "SUMMARY"];
4
4
 
5
5
  function escapeRegex(s) {
6
6
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
package/cli/team.mjs CHANGED
@@ -10,6 +10,7 @@ import { loadConfig } from "./config.mjs";
10
10
  import { getStoredApiKey } from "./login.mjs";
11
11
  import { resolveTeam, generateTeamPrompt } from "./agents.mjs";
12
12
  import { runOrchestrator } from "./orchestrator.mjs";
13
+ import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
13
14
 
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
@@ -31,7 +32,6 @@ function loadDotEnv(dir) {
31
32
  export async function runTeam(taskId, opts = {}) {
32
33
  const cwd = opts.cwd || process.cwd();
33
34
  const description = opts.description || "";
34
- const lite = opts.lite || false;
35
35
 
36
36
  // Detect project and resolve API key early (needed for server config fetch)
37
37
  const project = detectProject(cwd);
@@ -84,6 +84,39 @@ export async function runTeam(taskId, opts = {}) {
84
84
  const team = resolveTeam(config);
85
85
  const teamSections = generateTeamPrompt(team);
86
86
 
87
+ // --- Verify tracker permissions before starting session ---
88
+ if (tracker) {
89
+ const projectEnvVars = loadDotEnv(cwd);
90
+ let serverCreds = {};
91
+ if (apiKey) {
92
+ try {
93
+ const res = await fetch(`${agentdeskServer}/api/projects/${config.projectKey || project.name}/settings/credentials`, {
94
+ headers: { "x-api-key": apiKey },
95
+ signal: AbortSignal.timeout(5000),
96
+ });
97
+ if (res.ok) serverCreds = await res.json();
98
+ } catch {}
99
+ }
100
+
101
+ const credentials = resolveCredentialsFromEnv({ ...projectEnvVars, ...serverCreds });
102
+ const check = await checkTrackerPermissions({ tracker, config, credentials });
103
+
104
+ if (!check.ok) {
105
+ console.log("");
106
+ console.log("Tracker permission issues:");
107
+ for (const err of check.errors) {
108
+ console.log(` • ${err}`);
109
+ }
110
+ console.log("");
111
+ console.log("Fix the issues above or run 'agentdesk init' to reconfigure.");
112
+ console.log("To skip this check, set AGENTDESK_SKIP_TRACKER_CHECK=1");
113
+ if (!process.env.AGENTDESK_SKIP_TRACKER_CHECK) {
114
+ return 1;
115
+ }
116
+ console.log("Skipping check (AGENTDESK_SKIP_TRACKER_CHECK=1)...\n");
117
+ }
118
+ }
119
+
87
120
  // --- AgentDesk WebSocket config ---
88
121
  const AGENTDESK_URL = process.env.AGENTDESK_URL || "wss://agentdesk.live/ws/agent";
89
122
  const sessionId = `${taskId}-${randomUUID().slice(0, 8)}`;
@@ -125,26 +158,10 @@ export async function runTeam(taskId, opts = {}) {
125
158
  } else {
126
159
  console.log("AgentDesk: reconnected");
127
160
  }
128
- if (!lite) {
129
- // Orchestrator sends its own session:start — just flush queue
130
- sessionStartSent = true;
131
- while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
132
- vizWs.send(vizQueue.shift());
133
- }
134
- } else {
135
- vizSend({
136
- type: "session:start",
137
- taskId, taskLink,
138
- title: description || taskId,
139
- project: project.name || null,
140
- sessionNumber: 1,
141
- agents: teamSections.names,
142
- cliVersion: CLI_VERSION,
143
- });
144
- sessionStartSent = true;
145
- while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
146
- vizWs.send(vizQueue.shift());
147
- }
161
+ // Orchestrator sends its own session:start — just flush queue
162
+ sessionStartSent = true;
163
+ while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
164
+ vizWs.send(vizQueue.shift());
148
165
  }
149
166
  } else if (msg.type === "auth:error") {
150
167
  console.log("AgentDesk: authentication failed — run 'agentdesk login'");
@@ -169,14 +186,10 @@ export async function runTeam(taskId, opts = {}) {
169
186
 
170
187
  connectWs();
171
188
 
172
- const agentMode = lite ? "lite" : "full";
173
- console.log(`Agents: ${agentMode === "lite" ? "lite (shared process)" : "full (independent process)"}\n`);
174
-
175
189
  const result = await runOrchestrator({
176
190
  taskId, taskLink, description, createTask, tracker, config,
177
191
  project, team, teamSections, inboxUrl, sessionUrl, cwd,
178
192
  onEvent: vizSend,
179
- mode: agentMode,
180
193
  apiKey,
181
194
  serverUrl: agentdeskServer,
182
195
  });
@@ -0,0 +1,211 @@
1
+ // Verify tracker API permissions before starting a session
2
+ // Checks: read tasks, create tasks, update tasks
3
+
4
+ import { execSync } from "child_process";
5
+
6
+ const LINEAR_API = "https://api.linear.app/graphql";
7
+
8
+ /**
9
+ * Check tracker permissions. Returns { ok, errors[] }.
10
+ * Each error is a string describing what permission is missing.
11
+ */
12
+ export async function checkTrackerPermissions({ tracker, config, credentials }) {
13
+ if (!tracker) return { ok: true, errors: [] };
14
+
15
+ switch (tracker) {
16
+ case "linear":
17
+ return checkLinear(config.linear || {}, credentials);
18
+ case "jira":
19
+ return checkJira(config.jira || {}, credentials);
20
+ case "github":
21
+ return checkGitHub(config.github || {}, credentials);
22
+ default:
23
+ return { ok: true, errors: [] };
24
+ }
25
+ }
26
+
27
+ async function checkLinear({ teamKey, workspace }, creds) {
28
+ const apiKey = creds.LINEAR_API_KEY;
29
+ if (!apiKey) return { ok: false, errors: ["Missing LINEAR_API_KEY — configure it in the AgentDesk dashboard or .env"] };
30
+
31
+ const errors = [];
32
+
33
+ // Check read: fetch viewer and team info
34
+ try {
35
+ const viewerRes = await gql(apiKey, `{ viewer { id name } }`);
36
+ if (viewerRes.errors) {
37
+ return { ok: false, errors: ["LINEAR_API_KEY is invalid or expired"] };
38
+ }
39
+ } catch (e) {
40
+ return { ok: false, errors: [`Cannot reach Linear API: ${e.message}`] };
41
+ }
42
+
43
+ // Check read issues: list 1 issue from the team
44
+ if (teamKey) {
45
+ try {
46
+ const issuesRes = await gql(apiKey, `{ issues(filter: { team: { key: { eq: "${teamKey}" } } }, first: 1) { nodes { id identifier } } }`);
47
+ if (issuesRes.errors) {
48
+ errors.push(`Cannot read issues for team ${teamKey}: ${issuesRes.errors[0]?.message}`);
49
+ }
50
+ } catch {
51
+ errors.push(`Cannot read issues for team ${teamKey}`);
52
+ }
53
+ }
54
+
55
+ // Check create: attempt issueCreate dry-run — Linear doesn't have a dry-run mode,
56
+ // so we verify the team exists (which is required for creating issues)
57
+ if (teamKey) {
58
+ try {
59
+ const teamRes = await gql(apiKey, `{ teams(filter: { key: { eq: "${teamKey}" } }) { nodes { id name } } }`);
60
+ if (teamRes.errors || !teamRes.data?.teams?.nodes?.length) {
61
+ errors.push(`Team "${teamKey}" not found — cannot create issues`);
62
+ }
63
+ } catch {
64
+ errors.push(`Cannot verify team "${teamKey}" for issue creation`);
65
+ }
66
+ }
67
+
68
+ // Check update: verify we can read workflow states (needed for status transitions)
69
+ if (teamKey) {
70
+ try {
71
+ const statesRes = await gql(apiKey, `{ workflowStates(filter: { team: { key: { eq: "${teamKey}" } } }, first: 1) { nodes { id name } } }`);
72
+ if (statesRes.errors) {
73
+ errors.push(`Cannot read workflow states for team ${teamKey} — status updates will fail`);
74
+ }
75
+ } catch {
76
+ errors.push(`Cannot verify workflow states for team ${teamKey}`);
77
+ }
78
+ }
79
+
80
+ return { ok: errors.length === 0, errors };
81
+ }
82
+
83
+ async function checkJira({ baseUrl, project }, creds) {
84
+ const email = creds.JIRA_EMAIL;
85
+ const token = creds.JIRA_API_TOKEN;
86
+
87
+ if (!email || !token) return { ok: false, errors: ["Missing JIRA_EMAIL or JIRA_API_TOKEN — configure them in the AgentDesk dashboard or .env"] };
88
+ if (!baseUrl) return { ok: false, errors: ["Missing Jira base URL — run 'agentdesk init' to configure"] };
89
+
90
+ const errors = [];
91
+ const auth = "Basic " + Buffer.from(`${email}:${token}`).toString("base64");
92
+
93
+ // Use Jira's mypermissions endpoint to check all permissions at once
94
+ const permissionsToCheck = "BROWSE_PROJECTS,CREATE_ISSUES,EDIT_ISSUES,ADD_COMMENTS,TRANSITION_ISSUES";
95
+ try {
96
+ const url = `${baseUrl}/rest/api/3/mypermissions?permissions=${permissionsToCheck}` +
97
+ (project ? `&projectKey=${project}` : "");
98
+ const res = await fetch(url, {
99
+ headers: { Authorization: auth, Accept: "application/json" },
100
+ signal: AbortSignal.timeout(10000),
101
+ });
102
+
103
+ if (res.status === 401) {
104
+ return { ok: false, errors: ["Jira authentication failed — check JIRA_EMAIL and JIRA_API_TOKEN"] };
105
+ }
106
+ if (res.status === 403) {
107
+ return { ok: false, errors: ["Jira access forbidden — your account may lack access to this project"] };
108
+ }
109
+ if (!res.ok) {
110
+ return { ok: false, errors: [`Jira API error (${res.status}) — check your base URL: ${baseUrl}`] };
111
+ }
112
+
113
+ const data = await res.json();
114
+ const perms = data.permissions || {};
115
+
116
+ const permMap = {
117
+ BROWSE_PROJECTS: "read tasks",
118
+ CREATE_ISSUES: "create tasks",
119
+ EDIT_ISSUES: "update tasks",
120
+ ADD_COMMENTS: "add comments",
121
+ TRANSITION_ISSUES: "change task status",
122
+ };
123
+
124
+ for (const [key, label] of Object.entries(permMap)) {
125
+ if (perms[key] && !perms[key].havePermission) {
126
+ errors.push(`Missing permission: ${label} (${key})`);
127
+ }
128
+ }
129
+ } catch (e) {
130
+ if (e.name === "AbortError" || e.name === "TimeoutError") {
131
+ return { ok: false, errors: [`Cannot reach Jira at ${baseUrl} — request timed out`] };
132
+ }
133
+ return { ok: false, errors: [`Cannot reach Jira at ${baseUrl}: ${e.message}`] };
134
+ }
135
+
136
+ return { ok: errors.length === 0, errors };
137
+ }
138
+
139
+ async function checkGitHub({ repo }, creds) {
140
+ if (!repo) return { ok: false, errors: ["Missing GitHub repo — run 'agentdesk init' to configure"] };
141
+
142
+ const errors = [];
143
+
144
+ // Check if gh CLI is available
145
+ try {
146
+ execSync("gh --version", { stdio: "pipe" });
147
+ } catch {
148
+ return { ok: false, errors: ["GitHub CLI (gh) is not installed — install it from https://cli.github.com"] };
149
+ }
150
+
151
+ // Check auth status
152
+ try {
153
+ execSync("gh auth status", { stdio: "pipe" });
154
+ } catch {
155
+ return { ok: false, errors: ["GitHub CLI is not authenticated — run 'gh auth login'"] };
156
+ }
157
+
158
+ // Check repo access and permissions
159
+ try {
160
+ const result = execSync(`gh api repos/${repo} --jq ".permissions"`, { stdio: "pipe", encoding: "utf-8" });
161
+ const perms = JSON.parse(result.trim());
162
+
163
+ if (!perms.pull) {
164
+ errors.push("Missing permission: read issues (no pull access to repo)");
165
+ }
166
+ if (!perms.push) {
167
+ errors.push("Missing permission: create/update issues (no push access to repo)");
168
+ }
169
+ } catch (e) {
170
+ const msg = e.stderr?.toString() || e.message;
171
+ if (msg.includes("404") || msg.includes("Not Found")) {
172
+ errors.push(`Repository "${repo}" not found or not accessible`);
173
+ } else {
174
+ errors.push(`Cannot verify GitHub repo access: ${msg.trim()}`);
175
+ }
176
+ }
177
+
178
+ return { ok: errors.length === 0, errors };
179
+ }
180
+
181
+ // Helper: execute a Linear GraphQL query
182
+ async function gql(apiKey, query) {
183
+ const res = await fetch(LINEAR_API, {
184
+ method: "POST",
185
+ headers: {
186
+ Authorization: apiKey,
187
+ "Content-Type": "application/json",
188
+ },
189
+ body: JSON.stringify({ query }),
190
+ signal: AbortSignal.timeout(10000),
191
+ });
192
+
193
+ if (!res.ok) {
194
+ throw new Error(`Linear API returned ${res.status}`);
195
+ }
196
+
197
+ return res.json();
198
+ }
199
+
200
+ /**
201
+ * Resolve tracker credentials from available sources.
202
+ * Checks: env vars, .env file, server credentials.
203
+ */
204
+ export function resolveCredentialsFromEnv(env = {}) {
205
+ return {
206
+ LINEAR_API_KEY: env.LINEAR_API_KEY || process.env.LINEAR_API_KEY || undefined,
207
+ JIRA_EMAIL: env.JIRA_EMAIL || process.env.JIRA_EMAIL || undefined,
208
+ JIRA_API_TOKEN: env.JIRA_API_TOKEN || process.env.JIRA_API_TOKEN || undefined,
209
+ GITHUB_TOKEN: env.GITHUB_TOKEN || process.env.GITHUB_TOKEN || undefined,
210
+ };
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {