@kendoo.agentdesk/agentdesk 0.19.4 → 0.19.6

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,16 @@ 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.6] — 2026-04-19
17
+
18
+ ### Changed
19
+ - `[CLI]` `agentdesk bootstrap` now auto-selects the project silently when the git remote's repo name matches the project's id or name (case-insensitive, scope-stripped), in addition to the existing exact `github.repo` match. No confirmation prompt is shown for a confident single match — the clone just picks up the right project and continues. The numbered picker is only shown when auto-match finds zero or multiple candidates.
20
+
21
+ ## [0.19.5] — 2026-04-19
22
+
23
+ ### Changed
24
+ - `[CLI]` `agentdesk bootstrap` now works even when the cloned repo has no committed `.agentdesk.json`. If the project config file is absent, bootstrap fetches the user's project list from the server, tries to auto-match by the git remote (`origin`), and falls back to a numbered picker of the account's projects. Once picked, a minimal `.agentdesk.json` is written locally (prompting the user to commit it so future clones skip the picker) and the rest of the gap-scan continues unchanged. Previous behavior required the file to already be present.
25
+
16
26
  ## [0.19.4] — 2026-04-19
17
27
 
18
28
  ### Added
package/cli/bootstrap.mjs CHANGED
@@ -18,6 +18,7 @@
18
18
  import { existsSync, readFileSync, writeFileSync } from "fs";
19
19
  import { join } from "path";
20
20
  import { createInterface } from "readline";
21
+ import { execSync } from "child_process";
21
22
  import { loadConfig } from "./config.mjs";
22
23
  import { getStoredApiKey } from "./login.mjs";
23
24
  import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
@@ -25,6 +26,127 @@ import { assertPushable, PreflightError } from "./session-preflight.mjs";
25
26
 
26
27
  const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
27
28
 
29
+ function detectGitRemote(dir) {
30
+ try {
31
+ const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
32
+ const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
33
+ if (m) return `${m[1]}/${m[2]}`;
34
+ } catch {}
35
+ return null;
36
+ }
37
+
38
+ async function fetchProjects(apiKey) {
39
+ try {
40
+ const res = await fetch(`${SERVER}/api/projects`, {
41
+ headers: { "x-api-key": apiKey },
42
+ signal: AbortSignal.timeout(8000),
43
+ });
44
+ if (!res.ok) return null;
45
+ return await res.json();
46
+ } catch { return null; }
47
+ }
48
+
49
+ async function fetchProjectSettings(apiKey, projectId) {
50
+ try {
51
+ const res = await fetch(`${SERVER}/api/projects/${projectId}/settings`, {
52
+ headers: { "x-api-key": apiKey },
53
+ signal: AbortSignal.timeout(5000),
54
+ });
55
+ if (!res.ok) return {};
56
+ return await res.json();
57
+ } catch { return {}; }
58
+ }
59
+
60
+ // Normalize a name for fuzzy comparison: lowercase, strip npm scope (@scope/)
61
+ // and everything up to the last `/`, then drop non-alphanumerics.
62
+ function normalizeName(s) {
63
+ if (!s) return "";
64
+ const tail = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
65
+ return tail.toLowerCase().replace(/[^a-z0-9]/g, "");
66
+ }
67
+
68
+ // Discover the server-side project for this clone when .agentdesk.json is
69
+ // absent. Strategy (each tier that yields exactly one match auto-selects):
70
+ // Tier 1 — server-side github.repo equals the local git remote (owner/repo).
71
+ // Tier 2 — the repo name from the remote matches the project id or name
72
+ // (case-insensitive, scope-stripped).
73
+ // When 0 or 2+ projects match, fall back to a numbered picker.
74
+ // Returns the chosen projectKey, or null if the user aborts.
75
+ async function discoverProject(rl, cwd, apiKey) {
76
+ const remote = detectGitRemote(cwd);
77
+ if (remote) console.log(` Detected git remote: ${remote}`);
78
+ console.log(" Fetching your projects from the server...");
79
+
80
+ const projects = await fetchProjects(apiKey);
81
+ if (!projects || projects.length === 0) {
82
+ console.log("");
83
+ console.log(" No projects found on your account.");
84
+ console.log(" Run `agentdesk init` on the primary machine first, then retry here.");
85
+ console.log("");
86
+ return null;
87
+ }
88
+
89
+ // Fetch settings in parallel so we can display/match the configured repo.
90
+ const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
91
+ const remoteRepoName = remote ? remote.split("/").pop() : null;
92
+ const remoteKey = normalizeName(remoteRepoName);
93
+ const rows = projects.map((p, i) => {
94
+ const repo = settings[i]?.github?.repo || null;
95
+ const tier1 = remote && repo && repo.toLowerCase() === remote.toLowerCase();
96
+ const tier2 = !tier1 && remoteKey && (
97
+ normalizeName(p.id) === remoteKey || normalizeName(p.name) === remoteKey
98
+ );
99
+ return { project: p, settings: settings[i] || {}, repo, tier1, tier2 };
100
+ });
101
+
102
+ const tier1Matches = rows.filter(r => r.tier1);
103
+ const tier2Matches = rows.filter(r => r.tier2);
104
+ let chosen = null;
105
+ let matchReason = null;
106
+
107
+ if (tier1Matches.length === 1) {
108
+ chosen = tier1Matches[0];
109
+ matchReason = `github.repo matches ${chosen.repo}`;
110
+ } else if (tier1Matches.length === 0 && tier2Matches.length === 1) {
111
+ chosen = tier2Matches[0];
112
+ matchReason = `project name matches repo "${remoteRepoName}"`;
113
+ }
114
+
115
+ if (chosen) {
116
+ console.log(` ✓ Auto-matched: ${chosen.project.name} (${chosen.project.id}) — ${matchReason}`);
117
+ } else {
118
+ console.log("");
119
+ console.log(" Couldn't auto-match this clone to one of your projects.");
120
+ console.log("");
121
+ rows.forEach((r, i) => {
122
+ const tag = r.tier1 ? " ← repo match" : r.tier2 ? " ← name match" : "";
123
+ const repoStr = r.repo ? ` [${r.repo}]` : "";
124
+ console.log(` ${i + 1}) ${r.project.name} (${r.project.id})${repoStr}${tag}`);
125
+ });
126
+ console.log("");
127
+ while (true) {
128
+ const answer = await askInline(rl, ` Pick one (1-${rows.length}, or 'c' to cancel): `);
129
+ const trimmed = answer.trim().toLowerCase();
130
+ if (trimmed === "c" || trimmed === "cancel") return null;
131
+ const num = parseInt(trimmed, 10);
132
+ if (num >= 1 && num <= rows.length) { chosen = rows[num - 1]; break; }
133
+ console.log(` Please enter a number between 1 and ${rows.length}`);
134
+ }
135
+ }
136
+
137
+ // Write a minimal .agentdesk.json — loadConfig will merge server state
138
+ // into it on the next call, producing the full cached view.
139
+ const minimal = { projectKey: chosen.project.id };
140
+ writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(minimal, null, 2) + "\n");
141
+ console.log(` ✓ Wrote .agentdesk.json (projectKey: ${chosen.project.id})`);
142
+ console.log(" (commit this file so future clones skip discovery)");
143
+ return chosen.project.id;
144
+ }
145
+
146
+ function askInline(rl, question) {
147
+ return new Promise(resolve => rl.question(question, resolve));
148
+ }
149
+
28
150
  function ask(rl, question) {
29
151
  return new Promise(resolve => rl.question(question, resolve));
30
152
  }
@@ -149,26 +271,29 @@ export async function runBootstrap(cwd = process.cwd()) {
149
271
  process.exit(1);
150
272
  }
151
273
 
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
- }
274
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
275
+
276
+ // 2. Find the server-side project for this clone. Prefer a committed
277
+ // .agentdesk.json (clean path); fall back to discovering from the
278
+ // user's server-side project list when the file is missing.
279
+ let localConfig = readLocalConfig(cwd);
280
+ let projectKey = localConfig?.projectKey || null;
161
281
 
162
- const projectKey = localConfig.projectKey;
163
282
  if (!projectKey) {
164
- console.log(" .agentdesk.json has no projectKey — run `agentdesk init` to repair.");
283
+ if (localConfig) {
284
+ console.log(" .agentdesk.json has no projectKey — discovering project from server.");
285
+ } else {
286
+ console.log(" No .agentdesk.json in this directory — discovering project from server.");
287
+ }
288
+ console.log("");
289
+ projectKey = await discoverProject(rl, cwd, apiKey);
290
+ if (!projectKey) { rl.close(); process.exit(1); }
291
+ localConfig = readLocalConfig(cwd);
165
292
  console.log("");
166
- process.exit(1);
167
293
  }
168
294
 
169
295
  // 3. Load merged config (server-authoritative when reachable). This is the
170
296
  // same call `agentdesk team` makes, so we see the same view.
171
- const rl = createInterface({ input: process.stdin, output: process.stdout });
172
297
  const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
173
298
 
174
299
  const tracker = config.tracker || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {