@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 +4 -1
- package/cli/daemon.mjs +76 -22
- package/cli/init.mjs +5 -2
- package/cli/login.mjs +41 -3
- package/cli/projects.mjs +41 -6
- package/package.json +2 -2
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
|
-
## [
|
|
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
|
|
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
|
-
|
|
158
|
+
const creds = await ensureAccountIdentity();
|
|
159
|
+
const accountId = creds?.accountId || null;
|
|
156
160
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
},
|