@kendoo.agentdesk/agentdesk 0.19.3 → 0.19.4

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
@@ -13,6 +13,11 @@ Internal refactors, infrastructure changes, and architectural notes are not list
13
13
  ### Added
14
14
  - `[UI]` Private-session sharing flow. When someone visits a session URL they don't own, they now see a friendly "Shh… this one's private" page with a one-click "Request access" button instead of a blank error. The session owner sees pending requests in a new header inbox and can grant viewer access for just that session or the whole project. Viewer-granted sessions show up in the teammate's sidebar tagged as a viewer.
15
15
 
16
+ ## [0.19.4] — 2026-04-19
17
+
18
+ ### Added
19
+ - `[CLI]` `agentdesk bootstrap` — fresh-machine setup for repos already configured on another machine. Reads the committed `.agentdesk.json` for project identity, tracker type, team/project key, and GitHub repo, then only prompts for the per-machine secrets (Linear API key, Jira email + token, GitHub token) that belong in the local `.env`. Previously the only post-clone path was `agentdesk init`, which walked the full wizard and risked overwriting project-wide settings when the user just wanted to paste in their own tokens. Bootstrap never writes `.agentdesk.json` and never pushes to the server, so the first machine's setup stays untouched. `agentdesk login` now points at both `init` (new project) and `bootstrap` (cloned repo) as next steps.
20
+
16
21
  ## [0.19.3] — 2026-04-19
17
22
 
18
23
  ### Fixed
package/README.md CHANGED
@@ -47,6 +47,8 @@ A guided wizard walks you through it: picks up your project type, asks which tas
47
47
 
48
48
  Re-running `agentdesk init` in a project that already has config offers a "tracker only" shortcut. Use `agentdesk init --quick` to skip the narrative copy for scripted setups.
49
49
 
50
+ **On a second machine** (repo cloned from a teammate's setup): run `agentdesk bootstrap` instead of `init`. It reads the committed `.agentdesk.json` for the project-wide settings (tracker type, team/project key, repo) and only prompts for per-machine secrets (Linear API key, Jira email + token, GitHub token) that end up in `.env`. The other machine's `.env` and the server config are left untouched.
51
+
50
52
  `.agentdesk.json` is kept in sync with the server: every CLI run fetches the authoritative config and rewrites the local file as a credential-free snapshot. Credentials live only on the server, encrypted. If a local edit disagreed with the server's value, a one-line warning lists the overridden fields.
51
53
 
52
54
  ### 4. Run a team session
@@ -72,6 +74,7 @@ Watch the session live at [agentdesk.live](https://agentdesk.live). Each session
72
74
  agentdesk login Sign in to AgentDesk
73
75
  agentdesk logout Sign out and remove credentials
74
76
  agentdesk init Set up project and configure tracker
77
+ agentdesk bootstrap Fill in per-machine tokens on a cloned repo
75
78
  agentdesk team <TASK-ID> Run a team session on an existing task
76
79
  agentdesk team -d "..." Describe what you want — task created automatically
77
80
  agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
package/bin/agentdesk.mjs CHANGED
@@ -58,10 +58,16 @@ if (!command || command === "help" || command === "--help") {
58
58
  2. agentdesk init Set up your project (choose tracker, save config)
59
59
  3. agentdesk team TASK-123 Run a team session
60
60
 
61
+ On a freshly cloned repo (already set up on another machine):
62
+ 1. agentdesk login Sign in
63
+ 2. agentdesk bootstrap Fill in per-machine secrets (tokens)
64
+ 3. agentdesk team TASK-123 Run a team session
65
+
61
66
  Commands:
62
67
  agentdesk login Sign in to AgentDesk
63
68
  agentdesk logout Sign out and remove credentials
64
69
  agentdesk init [--quick] Set up project and configure tracker
70
+ agentdesk bootstrap Fill in per-machine tokens on a cloned repo
65
71
  agentdesk team <TASK-ID> Run a team session on an existing task
66
72
  agentdesk team -d "..." Create a task and run a session
67
73
  agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
@@ -146,6 +152,11 @@ else if (command === "init") {
146
152
  await runInit(process.cwd(), { quick });
147
153
  }
148
154
 
155
+ else if (command === "bootstrap") {
156
+ const { runBootstrap } = await import("../cli/bootstrap.mjs");
157
+ await runBootstrap(process.cwd());
158
+ }
159
+
149
160
  else if (command === "team") {
150
161
  // Parse options first to find -d, --cwd, --child-strategy
151
162
  let description = "";
@@ -0,0 +1,287 @@
1
+ // `agentdesk bootstrap` — fill in per-machine secrets after cloning a repo
2
+ // that's already been set up on a different machine.
3
+ //
4
+ // Intent: on a fresh clone, `.agentdesk.json` carries the project identity,
5
+ // tracker binding, and GitHub repo — everything that's project-wide. What's
6
+ // missing are the per-machine secrets that live in `.env` (LINEAR_API_KEY,
7
+ // JIRA_EMAIL/JIRA_API_TOKEN, GITHUB_TOKEN). Bootstrap detects which are
8
+ // missing or invalid and prompts only for those.
9
+ //
10
+ // Hard constraints:
11
+ // - Never write `.agentdesk.json` (besides the cached-server-state rewrite
12
+ // that loadConfig already does on every CLI run — we don't add new ones).
13
+ // - Never push to the server. Project-wide config on machine 1 is untouched.
14
+ // - Only writes to `.env`.
15
+ // - Idempotent. Running on a fully-configured machine prints "all set" and
16
+ // exits 0.
17
+
18
+ import { existsSync, readFileSync, writeFileSync } from "fs";
19
+ import { join } from "path";
20
+ import { createInterface } from "readline";
21
+ import { loadConfig } from "./config.mjs";
22
+ import { getStoredApiKey } from "./login.mjs";
23
+ import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
24
+ import { assertPushable, PreflightError } from "./session-preflight.mjs";
25
+
26
+ const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
27
+
28
+ function ask(rl, question) {
29
+ return new Promise(resolve => rl.question(question, resolve));
30
+ }
31
+
32
+ async function promptRequired(rl, label) {
33
+ while (true) {
34
+ const v = (await ask(rl, ` ${label}: `)).trim();
35
+ if (v) return v;
36
+ console.log(` ${label} is required. Ctrl+C to abort.`);
37
+ }
38
+ }
39
+
40
+ function saveEnvVar(dir, key, value) {
41
+ const envPath = join(dir, ".env");
42
+ let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
43
+ const re = new RegExp(`^${key}=.*$`, "m");
44
+ if (re.test(content)) {
45
+ content = content.replace(re, `${key}=${value}`);
46
+ } else {
47
+ content += `${content && !content.endsWith("\n") ? "\n" : ""}${key}=${value}\n`;
48
+ }
49
+ writeFileSync(envPath, content);
50
+ }
51
+
52
+ function loadDotEnvLocal(dir) {
53
+ const envPath = join(dir, ".env");
54
+ const out = {};
55
+ if (!existsSync(envPath)) return out;
56
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith("#")) continue;
59
+ const eq = trimmed.indexOf("=");
60
+ if (eq !== -1) out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function readLocalConfig(cwd) {
66
+ const path = join(cwd, ".agentdesk.json");
67
+ if (!existsSync(path)) return null;
68
+ try { return JSON.parse(readFileSync(path, "utf-8")); } catch { return null; }
69
+ }
70
+
71
+ // Compact step list — the init wizard carries the full narrative; bootstrap
72
+ // assumes the user has already seen it on their first machine.
73
+ function printTrackerHint(tracker) {
74
+ if (tracker === "linear") {
75
+ console.log(" (Get a Linear API key at https://linear.app/settings/api)");
76
+ } else if (tracker === "jira") {
77
+ console.log(" (Create a Jira API token at https://id.atlassian.com/manage-profile/security/api-tokens)");
78
+ } else if (tracker === "github") {
79
+ console.log(" (Create a GitHub token at https://github.com/settings/tokens — scope: repo)");
80
+ }
81
+ }
82
+
83
+ // Walk missing tracker credentials and prompt for each. Writes to .env.
84
+ // Returns the merged credentials map so the caller can verify.
85
+ async function promptTrackerCreds(rl, cwd, tracker, existing) {
86
+ const creds = { ...existing };
87
+ if (tracker === "linear") {
88
+ if (!creds.LINEAR_API_KEY) {
89
+ console.log("");
90
+ printTrackerHint("linear");
91
+ const v = await promptRequired(rl, "Linear API key");
92
+ saveEnvVar(cwd, "LINEAR_API_KEY", v);
93
+ creds.LINEAR_API_KEY = v;
94
+ console.log(" ✓ Saved LINEAR_API_KEY to .env");
95
+ }
96
+ } else if (tracker === "jira") {
97
+ if (!creds.JIRA_EMAIL) {
98
+ console.log("");
99
+ printTrackerHint("jira");
100
+ const v = await promptRequired(rl, "Your Atlassian login email");
101
+ saveEnvVar(cwd, "JIRA_EMAIL", v);
102
+ creds.JIRA_EMAIL = v;
103
+ console.log(" ✓ Saved JIRA_EMAIL to .env");
104
+ }
105
+ if (!creds.JIRA_API_TOKEN) {
106
+ if (creds.JIRA_EMAIL) console.log("");
107
+ const v = await promptRequired(rl, "Jira API token");
108
+ saveEnvVar(cwd, "JIRA_API_TOKEN", v);
109
+ creds.JIRA_API_TOKEN = v;
110
+ console.log(" ✓ Saved JIRA_API_TOKEN to .env");
111
+ }
112
+ }
113
+ return creds;
114
+ }
115
+
116
+ async function promptGitHubToken(rl, cwd) {
117
+ console.log("");
118
+ printTrackerHint("github");
119
+ const v = await promptRequired(rl, "GitHub token");
120
+ saveEnvVar(cwd, "GITHUB_TOKEN", v);
121
+ console.log(" ✓ Saved GITHUB_TOKEN to .env");
122
+ return v;
123
+ }
124
+
125
+ async function selectOption(rl, prompt, options) {
126
+ console.log(` ${prompt}`);
127
+ console.log("");
128
+ options.forEach((opt, i) => console.log(` ${i + 1}) ${opt.label}`));
129
+ console.log("");
130
+ while (true) {
131
+ const answer = await ask(rl, ` Choose (1-${options.length}): `);
132
+ const num = parseInt(answer.trim(), 10);
133
+ if (num >= 1 && num <= options.length) return options[num - 1];
134
+ console.log(` Please enter a number between 1 and ${options.length}`);
135
+ }
136
+ }
137
+
138
+ export async function runBootstrap(cwd = process.cwd()) {
139
+ console.log("");
140
+ console.log(" AgentDesk — Bootstrap this machine");
141
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
142
+ console.log("");
143
+
144
+ // 1. Must be logged in.
145
+ const apiKey = getStoredApiKey();
146
+ if (!apiKey) {
147
+ console.log(" Not logged in. Run `agentdesk login` first.");
148
+ console.log("");
149
+ process.exit(1);
150
+ }
151
+
152
+ // 2. Must have a project config committed in this repo.
153
+ const localConfig = readLocalConfig(cwd);
154
+ if (!localConfig) {
155
+ console.log(" No .agentdesk.json in this directory.");
156
+ console.log(" This command is for repos that were already set up on another machine.");
157
+ console.log(" For a new project, run: agentdesk init");
158
+ console.log("");
159
+ process.exit(1);
160
+ }
161
+
162
+ const projectKey = localConfig.projectKey;
163
+ if (!projectKey) {
164
+ console.log(" .agentdesk.json has no projectKey — run `agentdesk init` to repair.");
165
+ console.log("");
166
+ process.exit(1);
167
+ }
168
+
169
+ // 3. Load merged config (server-authoritative when reachable). This is the
170
+ // same call `agentdesk team` makes, so we see the same view.
171
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
172
+ const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
173
+
174
+ const tracker = config.tracker || null;
175
+ const repo = config.github?.repo || null;
176
+ const login = config.github?.login || null;
177
+
178
+ console.log(` Project: ${projectKey}`);
179
+ console.log(` Tracker: ${tracker || "none"}`);
180
+ if (tracker === "linear" && config.linear?.teamKey) console.log(` Team: ${config.linear.teamKey}`);
181
+ if (tracker === "jira" && config.jira?.project) console.log(` Project: ${config.jira.project}`);
182
+ if (repo) console.log(` Repo: ${repo}${login ? ` (@${login})` : ""}`);
183
+ console.log("");
184
+
185
+ // 4. Gap scan + prompt loop. We retry tracker + github verification up to
186
+ // three times, matching init's retry/skip/cancel UX.
187
+ let creds = resolveCredentialsFromEnv(loadDotEnvLocal(cwd));
188
+ let trackerOk = !tracker; // no tracker → nothing to verify
189
+ let githubOk = false;
190
+ let attempts = 0;
191
+
192
+ while (attempts < 5) {
193
+ attempts += 1;
194
+
195
+ // Tracker
196
+ if (tracker && !trackerOk) {
197
+ const missing =
198
+ (tracker === "linear" && !creds.LINEAR_API_KEY) ||
199
+ (tracker === "jira" && (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN));
200
+ if (missing) {
201
+ creds = await promptTrackerCreds(rl, cwd, tracker, creds);
202
+ }
203
+ process.stdout.write(" Verifying tracker access... ");
204
+ const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
205
+ if (check.ok) {
206
+ console.log("✓");
207
+ if (check.identity?.name || check.identity?.email) {
208
+ const label = check.identity.name || check.identity.login || check.identity.email;
209
+ console.log(` ✓ Posting as: ${label}`);
210
+ }
211
+ trackerOk = true;
212
+ } else {
213
+ console.log("failed");
214
+ for (const e of check.errors || []) console.log(` • ${e}`);
215
+ console.log("");
216
+ const next = await selectOption(rl, "What now?", [
217
+ { label: "Re-enter credentials", value: "retry" },
218
+ { label: "Skip tracker (fix later in .env)", value: "skip" },
219
+ { label: "Cancel", value: "cancel" },
220
+ ]);
221
+ console.log("");
222
+ if (next.value === "cancel") { rl.close(); process.exit(1); }
223
+ if (next.value === "skip") { trackerOk = true; break; }
224
+ // retry: clear the tracker-specific creds so the next loop re-prompts
225
+ if (tracker === "linear") delete creds.LINEAR_API_KEY;
226
+ if (tracker === "jira") { delete creds.JIRA_EMAIL; delete creds.JIRA_API_TOKEN; }
227
+ continue;
228
+ }
229
+ }
230
+
231
+ // GitHub — always required (agents push code regardless of tracker)
232
+ if (!githubOk) {
233
+ if (!creds.GITHUB_TOKEN) {
234
+ creds.GITHUB_TOKEN = await promptGitHubToken(rl, cwd);
235
+ }
236
+ try {
237
+ assertPushable({ cwd, creds: { GITHUB_TOKEN: creds.GITHUB_TOKEN }, projectName: projectKey });
238
+ // Best-effort gh API check to confirm the token is valid and matches
239
+ // the configured login. Mirrors init.mjs:495-506.
240
+ try {
241
+ const { execSync } = await import("child_process");
242
+ const env = { ...process.env, GH_TOKEN: creds.GITHUB_TOKEN };
243
+ const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
244
+ if (who && login && who.toLowerCase() !== login.toLowerCase()) {
245
+ console.log(` ⚠ GitHub token authenticates as @${who}, not @${login} (from .agentdesk.json). Agents will post as @${who}.`);
246
+ } else if (who) {
247
+ console.log(` ✓ GitHub verified as @${who}`);
248
+ }
249
+ } catch {
250
+ console.log(" ✓ GITHUB_TOKEN present (couldn't verify via gh — install gh CLI for identity check)");
251
+ }
252
+ githubOk = true;
253
+ } catch (err) {
254
+ if (err instanceof PreflightError) {
255
+ console.log(` ✗ ${err.message}`);
256
+ console.log("");
257
+ const next = await selectOption(rl, "What now?", [
258
+ { label: "Re-enter GITHUB_TOKEN", value: "retry" },
259
+ { label: "Skip (sessions will fail until fixed)", value: "skip" },
260
+ { label: "Cancel", value: "cancel" },
261
+ ]);
262
+ console.log("");
263
+ if (next.value === "cancel") { rl.close(); process.exit(1); }
264
+ if (next.value === "skip") { githubOk = true; break; }
265
+ delete creds.GITHUB_TOKEN;
266
+ continue;
267
+ }
268
+ throw err;
269
+ }
270
+ }
271
+
272
+ if (trackerOk && githubOk) break;
273
+ }
274
+
275
+ rl.close();
276
+
277
+ console.log("");
278
+ if (trackerOk && githubOk) {
279
+ console.log(" Ready. Try:");
280
+ const prefix = config.linear?.teamKey || config.jira?.project || projectKey.toUpperCase();
281
+ console.log(` agentdesk team ${prefix}-123`);
282
+ console.log("");
283
+ } else {
284
+ console.log(" Bootstrap finished with missing pieces — edit .env directly or re-run.");
285
+ console.log("");
286
+ }
287
+ }
package/cli/login.mjs CHANGED
@@ -114,7 +114,8 @@ export async function runLogin() {
114
114
  console.log(` API key saved to ~/.agentdesk/credentials.json`);
115
115
  console.log("");
116
116
  console.log(" Next steps:");
117
- console.log(" agentdesk init");
117
+ console.log(" agentdesk init (new project on this machine)");
118
+ console.log(" agentdesk bootstrap (repo cloned from another machine)");
118
119
  console.log(" agentdesk team TASK-123");
119
120
  console.log("");
120
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.19.3",
3
+ "version": "0.19.4",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {