@kendoo.agentdesk/agentdesk 0.26.0 → 0.27.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.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,7 @@ 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
- ## [Unreleased]
11
+ ## [0.27.0] — 2026-07-06
12
12
 
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.
@@ -16,6 +16,9 @@ Internal refactors, infrastructure changes, and architectural notes are not list
16
16
  ### Changed
17
17
  - `[Both]` Security improvements and hardening. No action required.
18
18
 
19
+ ### Fixed
20
+ - `[CLI]` The daemon no longer shows projects from other accounts after switching logins on the same machine. Project registrations are now tied to the account that created them, and the daemon cross-checks the server's per-account project list on startup. Projects from before this fix are matched to your account automatically on the next daemon start.
21
+
19
22
  ## [0.22.0] — 2026-05-23
20
23
 
21
24
  ### Changed
package/cli/daemon.mjs CHANGED
@@ -8,12 +8,12 @@ import { randomUUID } from "crypto";
8
8
  import WebSocket from "ws";
9
9
  import { detectProject } from "./detect.mjs";
10
10
  import { loadConfig } from "./config.mjs";
11
- import { getStoredApiKey } from "./login.mjs";
11
+ import { getStoredApiKey, ensureAccountIdentity } from "./login.mjs";
12
12
  import { resolveTeam, generateTeamPrompt } from "./agents.mjs";
13
13
  import { buildPrompt } from "./prompt.mjs";
14
14
  import { createStreamParser } from "./stream-parser.mjs";
15
15
  import { runOrchestrator, runPhasedOrchestrator } from "./orchestrator.mjs";
16
- import { getRegisteredProjects, registerLocalProject } from "./projects.mjs";
16
+ import { getRegisteredProjects, registerLocalProject, claimLocalProjects } from "./projects.mjs";
17
17
  import { buildTrackerUrl } from "./tracker-url.mjs";
18
18
  import { fileURLToPath } from "url";
19
19
  import { dirname } from "path";
@@ -150,46 +150,100 @@ export async function runDaemon() {
150
150
  process.exit(1);
151
151
  }
152
152
 
153
- // 2. Load registered projects local registry first, then server fallback
153
+ // 2. Load registered projects, scoped to the logged-in account (AD-64).
154
+ // The local registry is machine-global — it accumulates projects from
155
+ // every account ever used on this machine — so the server's per-account
156
+ // project list is the authority on what this account may see.
154
157
  const agentdeskServer = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
155
- let allProjects = getRegisteredProjects();
158
+ const creds = await ensureAccountIdentity();
159
+ const accountId = creds?.accountId || null;
156
160
 
157
- // Fallback: fetch from server if local registry is empty (pre-0.7.0 projects)
158
- if (allProjects.length === 0) {
159
- try {
160
- const res = await fetch(`${agentdeskServer}/api/projects`, {
161
- headers: { "x-api-key": apiKey },
162
- signal: AbortSignal.timeout(5000),
163
- });
164
- if (res.ok) {
165
- const serverProjects = await res.json();
166
- if (Array.isArray(serverProjects) && serverProjects.length > 0) {
167
- console.log(` ${dim}Syncing ${serverProjects.length} project(s) from server...${reset}`);
168
- for (const sp of serverProjects) {
161
+ let serverIds = null;
162
+ try {
163
+ const res = await fetch(`${agentdeskServer}/api/projects`, {
164
+ headers: { "x-api-key": apiKey },
165
+ signal: AbortSignal.timeout(5000),
166
+ });
167
+ if (res.ok) {
168
+ const serverProjects = await res.json();
169
+ if (Array.isArray(serverProjects)) {
170
+ serverIds = new Set(serverProjects.map(sp => sp.id || sp.name));
171
+
172
+ // Untagged local entries with no server row — either they predate
173
+ // server-side registration or were initialized under another login.
174
+ // Offer them to the server: it accepts free ids and silently no-ops
175
+ // ids owned by another account (AD-23), so re-fetching the list
176
+ // tells us which ones are actually ours.
177
+ const unclaimed = getRegisteredProjects().filter(p =>
178
+ !p.accountId && !serverIds.has(p.id) &&
179
+ existsSync(p.path) && existsSync(join(p.path, ".agentdesk.json")));
180
+ if (unclaimed.length > 0) {
181
+ for (const p of unclaimed) {
182
+ try {
183
+ await fetch(`${agentdeskServer}/api/projects`, {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
186
+ body: JSON.stringify({ id: p.id, name: p.name, path: p.path }),
187
+ signal: AbortSignal.timeout(5000),
188
+ });
189
+ } catch {}
190
+ }
191
+ try {
192
+ const recheck = await fetch(`${agentdeskServer}/api/projects`, {
193
+ headers: { "x-api-key": apiKey },
194
+ signal: AbortSignal.timeout(5000),
195
+ });
196
+ if (recheck.ok) {
197
+ const confirmed = await recheck.json();
198
+ if (Array.isArray(confirmed)) {
199
+ serverIds = new Set(confirmed.map(sp => sp.id || sp.name));
200
+ }
201
+ }
202
+ } catch {}
203
+ for (const p of unclaimed) {
204
+ if (!serverIds.has(p.id)) {
205
+ console.log(` ${yellow}Skipping ${p.name}${reset} ${dim}— registered to a different AgentDesk account${reset}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ // Claim legacy untagged local entries the server confirmed as ours
211
+ claimLocalProjects([...serverIds], accountId);
212
+
213
+ // Sync server projects missing from the local registry (pre-0.7.0
214
+ // setups, or projects initialized on another machine)
215
+ const localIds = new Set(getRegisteredProjects().map(p => p.id));
216
+ const missing = serverProjects.filter(sp => !localIds.has(sp.id || sp.name));
217
+ if (missing.length > 0) {
218
+ console.log(` ${dim}Syncing ${missing.length} project(s) from server...${reset}`);
219
+ for (const sp of missing) {
169
220
  const projectName = sp.id || sp.name;
170
221
  // If server has a valid path, use it
171
222
  if (sp.path && existsSync(sp.path)) {
172
- registerLocalProject(projectName, sp.name, sp.path);
223
+ registerLocalProject(projectName, sp.name, sp.path, accountId);
173
224
  continue;
174
225
  }
175
226
  // Server has empty path — try to find the project locally
176
227
  const found = findProjectLocally(projectName);
177
228
  if (found) {
178
229
  console.log(` ${dim}Found ${projectName} at ${found}${reset}`);
179
- registerLocalProject(projectName, sp.name, found);
230
+ registerLocalProject(projectName, sp.name, found, accountId);
180
231
  } else {
181
232
  console.log(` ${yellow}Could not find ${projectName} locally.${reset} Run ${cyan}agentdesk init${reset} in its directory.`);
182
233
  }
183
234
  }
184
- allProjects = getRegisteredProjects();
185
235
  }
186
236
  }
187
- } catch {
188
- // Server not reachable — continue with local only
189
237
  }
238
+ } catch {
239
+ // Server not reachable — fall back to account-tagged local entries
190
240
  }
191
241
 
192
- const projects = allProjects.filter(p => {
242
+ const projects = getRegisteredProjects(accountId).filter(p => {
243
+ // Server reachable → it is authoritative: hide entries it doesn't own,
244
+ // even legacy untagged ones (they may belong to another account).
245
+ // Offline → tagged + untagged entries from getRegisteredProjects().
246
+ if (serverIds && !serverIds.has(p.id) && !(accountId && p.accountId === accountId)) return false;
193
247
  if (!existsSync(p.path)) return false;
194
248
  if (!existsSync(join(p.path, ".agentdesk.json"))) return false;
195
249
  return true;
package/cli/init.mjs CHANGED
@@ -20,7 +20,7 @@
20
20
  import { existsSync, readFileSync, writeFileSync } from "fs";
21
21
  import { join } from "path";
22
22
  import { loadConfig, pushConfig } from "./config.mjs";
23
- import { getStoredApiKey } from "./login.mjs";
23
+ import { getStoredApiKey, ensureAccountIdentity } from "./login.mjs";
24
24
  import { registerLocalProject } from "./projects.mjs";
25
25
  import { checkTrackerPermissions } from "./tracker-check.mjs";
26
26
  import { autoMatchProject } from "./bootstrap.mjs";
@@ -535,7 +535,10 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
535
535
  }
536
536
  }
537
537
 
538
- registerLocalProject(computedKey, projectName, currentCwd);
538
+ // AD-64: tag the registry entry with the account so the daemon can scope
539
+ // its project list per login.
540
+ const accountId = (await ensureAccountIdentity())?.accountId || null;
541
+ registerLocalProject(computedKey, projectName, currentCwd, accountId);
539
542
 
540
543
  console.log("");
541
544
  const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
package/cli/login.mjs CHANGED
@@ -17,13 +17,42 @@ function getEnvApiKey() {
17
17
  }
18
18
 
19
19
  export function getStoredApiKey() {
20
+ return getStoredCredentials()?.apiKey || null;
21
+ }
22
+
23
+ export function getStoredCredentials() {
20
24
  if (!existsSync(CREDENTIALS_PATH)) return null;
21
25
  try {
22
- const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
23
- return creds.apiKey || null;
26
+ return JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
27
+ } catch { return null; }
28
+ }
29
+
30
+ // AD-64: resolve which account an API key belongs to, so local project
31
+ // registry entries can be scoped per account.
32
+ async function fetchAccountIdentity(apiKey) {
33
+ try {
34
+ const res = await fetch(`${AGENTDESK_SERVER}/api/me`, {
35
+ headers: { "x-api-key": apiKey },
36
+ signal: AbortSignal.timeout(5000),
37
+ });
38
+ if (!res.ok) return null;
39
+ const me = await res.json();
40
+ return me?.id ? { accountId: me.id, email: me.email || null } : null;
24
41
  } catch { return null; }
25
42
  }
26
43
 
44
+ // Backfill accountId into credentials saved by pre-AD-64 versions.
45
+ // Returns the (possibly updated) credentials object.
46
+ export async function ensureAccountIdentity() {
47
+ const creds = getStoredCredentials();
48
+ if (!creds?.apiKey || creds.accountId) return creds;
49
+ const identity = await fetchAccountIdentity(creds.apiKey);
50
+ if (!identity) return creds;
51
+ const updated = { ...creds, ...identity };
52
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(updated, null, 2), { mode: 0o600 });
53
+ return updated;
54
+ }
55
+
27
56
  export async function runLogin() {
28
57
  console.log("");
29
58
  console.log(" AgentDesk — Login");
@@ -120,7 +149,16 @@ export async function runLogin() {
120
149
  console.log(" agentdesk team TASK-123");
121
150
  console.log("");
122
151
 
123
- setTimeout(() => { server.close(); process.exit(0); }, 500);
152
+ // AD-64: attach the account identity so the project registry can be
153
+ // scoped per account. Best-effort — login still succeeds without it.
154
+ fetchAccountIdentity(apiKey)
155
+ .then(identity => {
156
+ if (identity) {
157
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey, name, ...identity, savedAt: Date.now() }, null, 2), { mode: 0o600 });
158
+ }
159
+ })
160
+ .catch(() => {})
161
+ .finally(() => { server.close(); process.exit(0); });
124
162
  } else {
125
163
  res.writeHead(400, { "Content-Type": "text/html" });
126
164
  res.end("<html><body><h2>Login failed. No API key received.</h2></body></html>");
package/cli/projects.mjs CHANGED
@@ -1,4 +1,9 @@
1
1
  // Local project registry — tracks which projects have been initialized with `agentdesk init`
2
+ //
3
+ // AD-64: entries are tagged with the accountId they were registered under so
4
+ // the daemon can scope its project list to the logged-in account. Entries
5
+ // written by older versions have no accountId ("untagged") — the daemon
6
+ // claims them for the active account when the server confirms ownership.
2
7
 
3
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
9
  import { join } from "path";
@@ -6,7 +11,7 @@ import { join } from "path";
6
11
  const CONFIG_DIR = join(process.env.HOME || process.env.USERPROFILE, ".agentdesk");
7
12
  const PROJECTS_PATH = join(CONFIG_DIR, "projects.json");
8
13
 
9
- export function getRegisteredProjects() {
14
+ function readRegistry() {
10
15
  try {
11
16
  if (!existsSync(PROJECTS_PATH)) return [];
12
17
  const data = JSON.parse(readFileSync(PROJECTS_PATH, "utf-8"));
@@ -16,10 +21,28 @@ export function getRegisteredProjects() {
16
21
  }
17
22
  }
18
23
 
19
- export function registerLocalProject(id, name, path) {
20
- const projects = getRegisteredProjects();
24
+ function writeRegistry(projects) {
25
+ if (!existsSync(CONFIG_DIR)) {
26
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
27
+ }
28
+ writeFileSync(PROJECTS_PATH, JSON.stringify({ projects }, null, 2) + "\n", { mode: 0o600 });
29
+ }
30
+
31
+ // accountId: return only that account's entries plus legacy untagged ones
32
+ // (callers that can reach the server should claimLocalProjects() first so
33
+ // untagged entries get resolved rather than leaking across accounts).
34
+ export function getRegisteredProjects(accountId) {
35
+ const projects = readRegistry();
36
+ if (!accountId) return projects;
37
+ return projects.filter(p => !p.accountId || p.accountId === accountId);
38
+ }
39
+
40
+ export function registerLocalProject(id, name, path, accountId) {
41
+ const projects = readRegistry();
21
42
  const existing = projects.findIndex(p => p.id === id);
22
43
  const entry = { id, name, path, registeredAt: Date.now() };
44
+ if (accountId) entry.accountId = accountId;
45
+ else if (existing >= 0 && projects[existing].accountId) entry.accountId = projects[existing].accountId;
23
46
 
24
47
  if (existing >= 0) {
25
48
  projects[existing] = entry;
@@ -27,8 +50,20 @@ export function registerLocalProject(id, name, path) {
27
50
  projects.push(entry);
28
51
  }
29
52
 
30
- if (!existsSync(CONFIG_DIR)) {
31
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
53
+ writeRegistry(projects);
54
+ }
55
+
56
+ // Tag untagged entries whose ids the server confirmed belong to accountId.
57
+ export function claimLocalProjects(ids, accountId) {
58
+ if (!accountId || !ids?.length) return;
59
+ const idSet = new Set(ids);
60
+ const projects = readRegistry();
61
+ let changed = false;
62
+ for (const p of projects) {
63
+ if (!p.accountId && idSet.has(p.id)) {
64
+ p.accountId = accountId;
65
+ changed = true;
66
+ }
32
67
  }
33
- writeFileSync(PROJECTS_PATH, JSON.stringify({ projects }, null, 2) + "\n", { mode: 0o600 });
68
+ if (changed) writeRegistry(projects);
34
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "server": "node server/index.mjs",
23
23
  "build": "vite build",
24
24
  "preview": "vite preview",
25
- "test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs",
25
+ "test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs tests/projects-registry.test.mjs",
26
26
  "lint:changelog": "node scripts/lint-changelog.mjs",
27
27
  "prepublishOnly": "node scripts/lint-changelog.mjs"
28
28
  },