@kendoo.agentdesk/agentdesk 0.25.2 → 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/README.md CHANGED
@@ -123,6 +123,7 @@ agentdesk team <TASK-ID> Run a team session on an existing task
123
123
  agentdesk team -d "..." Describe what you want — task created automatically
124
124
  agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
125
125
  agentdesk daemon Start daemon for remote sessions
126
+ agentdesk security-check [-d <dir>] Multi-surface security audit (server / CLI / UI / prompts)
126
127
  agentdesk update Update to the latest version
127
128
  ```
128
129
 
package/bin/agentdesk.mjs CHANGED
@@ -74,6 +74,7 @@ if (!command || command === "help" || command === "--help") {
74
74
  agentdesk team -d "..." Create a task and run a session
75
75
  agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
76
76
  agentdesk daemon Start daemon for remote sessions
77
+ agentdesk security-check [-d <dir>] Multi-surface security audit (server / CLI / UI / prompts)
77
78
  agentdesk update Update to the latest version
78
79
 
79
80
  Options:
@@ -207,6 +208,13 @@ else if (command === "daemon") {
207
208
  await runDaemon();
208
209
  }
209
210
 
211
+ else if (command === "security-check") {
212
+ const { runSecurityCheck, parseSecurityCheckArgs } = await import("../cli/security-check.mjs");
213
+ const opts = parseSecurityCheckArgs(args.slice(1));
214
+ const code = await runSecurityCheck(opts);
215
+ process.exit(code);
216
+ }
217
+
210
218
  else if (command === "update") {
211
219
  const { execSync } = await import("child_process");
212
220
  const currentVersion = pkg.version;
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
  }
@@ -0,0 +1,187 @@
1
+ // `agentdesk security-check` — runs a multi-surface security audit on a
2
+ // codebase by spawning a Claude process scoped to the target directory.
3
+ //
4
+ // The audit pattern: identify which surfaces exist (server / CLI / UI /
5
+ // prompts), spawn parallel investigators per surface via Claude's Agent
6
+ // tool, return one triaged punch list (Critical / High / Medium / Low)
7
+ // with file:line citations and concrete fix recommendations.
8
+ //
9
+ // This is a batch full-codebase audit, NOT a PR/diff review. For diff
10
+ // review use Claude Code's built-in `/security-review` skill instead.
11
+
12
+ import { spawn } from "child_process";
13
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
14
+ import { resolve, join } from "path";
15
+
16
+ const PROMPT = `You are running a multi-surface security audit on the codebase in the current working directory.
17
+
18
+ Goal: produce one triaged punch list of security findings (Critical / High / Medium / Low) with file:line citations and concrete fix recommendations.
19
+
20
+ Step 1 — Survey what's here:
21
+ - Server-shaped dirs: server/, api/, backend/, services/, plus *.py / *.go / *.rs / *.mjs / *.ts that look like HTTP/WS server entrypoints.
22
+ - CLI-shaped dirs: bin/, cli/, cmd/, top-level scripts.
23
+ - UI-shaped dirs: src/, app/, web/, frontend/, components/, index.html.
24
+ - Prompts / config inlined into LLM calls: prompts/, .claude/, CLAUDE.md, MCP configs, agent skill files.
25
+
26
+ Tell the user which surfaces you'll cover before launching investigators. Skip any that don't exist.
27
+
28
+ Step 2 — Spawn parallel investigators (one per existing surface) via the Agent tool with subagent_type "general-purpose". Send them in a single message with multiple tool calls so they run concurrently.
29
+
30
+ Each investigator gets the surface path and a focused checklist:
31
+
32
+ Server / API — auth bypass (missing auth gates, JWT weaknesses, CSRF, session fixation), authorization / IDOR on every :id route, input validation / injection (SQL via string concat, command injection, path traversal, SSRF, eval, prototype pollution), credential handling (encryption mode, IV reuse, key derivation, leak surfaces), rate limiting on auth endpoints, WebSocket auth (auth-by-message is non-standard), dependency vulns, secret exposure, error handling that leaks stack traces.
33
+
34
+ CLI / daemon — command injection (every exec/execSync template-literal sink), path traversal in fs.writeFile / path.join, sandbox claims vs implementation (denylists are usually wrong), credential exposure (tokens in logs/argv/errors/temp files), URL parsing SSRF, prompt-injection vectors when CLI feeds external content into an LLM, daemon-push abuse (signed? confirmation required?), config/credential precedence bugs.
35
+
36
+ UI / frontend / prompts — XSS in any dangerouslySetInnerHTML or markdown render of user-controlled content (agent output is untrusted), JWT/secrets in localStorage, bundled VITE_/NEXT_PUBLIC env vars in the public bundle, CSRF / SameSite, open redirect / clickjacking / missing frame-ancestors, <a href> without scheme allowlist (javascript: URLs run same-origin), prompt-injection robustness (untrusted-data delimiters? hard system rule against following directives in user content?), dependency vulns, CSP / security headers.
37
+
38
+ Each investigator returns a triaged list (Critical / High / Medium / Low) with file:line citations and concrete fix recommendations. Cap their responses at ~700 words; tell them to prioritize ruthlessly.
39
+
40
+ Step 3 — Synthesize:
41
+ - Group findings by severity (not by surface — the user wants the queue, not the org chart).
42
+ - Look for chains where a Medium reaches a Critical — note them.
43
+ - Identify the 2-3 structural patterns driving the findings (e.g., "untrusted data treated as trusted across boundaries", "sandbox claim doesn't match implementation"). These root causes are more useful than per-line fixes.
44
+ - Propose a triage plan in dependency order (which fixes ship together, what order minimizes the exposure window).
45
+
46
+ Step 4 — Present:
47
+
48
+ # Security audit — <project name> — <date>
49
+
50
+ ## Headline numbers
51
+ - N Critical (exploitable as-is)
52
+ - N High (real risk, needs a chain or specific condition)
53
+ - N Medium (defense-in-depth gaps)
54
+ - N Low (hardening opportunities)
55
+
56
+ ## The N things to fix this week
57
+ <top items with file:line + why-exploitable + concrete fix>
58
+
59
+ ## Full punch list (compact)
60
+ ### CRITICAL
61
+ - file.ext:LL — short description. Why exploitable: ... Suggested fix: ...
62
+ ### HIGH ...
63
+ ### MEDIUM ...
64
+ ### LOW ...
65
+
66
+ ## The pattern
67
+ <2-3 sentence root-cause analysis>
68
+
69
+ ## Suggested triage plan
70
+ <ordered plan with rough day-by-day>
71
+
72
+ Important:
73
+ - Do NOT fix anything. Audit and report only.
74
+ - Be honest about scope — a real audit is days. This one pass produces a focused triage list.
75
+ - For any CHANGELOG entry referencing security work, use NEUTRAL phrasing ("Security improvements. No action required.") — never name the bug class on public release notes.
76
+ `;
77
+
78
+ function colorize() {
79
+ const isTTY = process.stdout.isTTY;
80
+ const c = (code) => (s) => isTTY ? `\x1b[${code}m${s}\x1b[0m` : s;
81
+ return { dim: c(2), green: c(32), cyan: c(36), yellow: c(33), red: c(31) };
82
+ }
83
+
84
+ export async function runSecurityCheck({ cwd, out }) {
85
+ const { dim, green, cyan, yellow, red } = colorize();
86
+ const targetDir = resolve(cwd || process.cwd());
87
+ if (!existsSync(targetDir)) {
88
+ console.error(`${red("Directory not found:")} ${targetDir}`);
89
+ return 1;
90
+ }
91
+
92
+ // Default output path: .agentdesk/security-audit-<timestamp>.md inside the audited project.
93
+ const stamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 16);
94
+ const outPath = resolve(out || join(targetDir, ".agentdesk", `security-audit-${stamp}.md`));
95
+ mkdirSync(join(outPath, ".."), { recursive: true });
96
+
97
+ console.log("");
98
+ console.log(` ${green("agentdesk security-check")}`);
99
+ console.log(` ${dim("Target:")} ${targetDir}`);
100
+ console.log(` ${dim("Report:")} ${outPath}`);
101
+ console.log("");
102
+ console.log(` ${dim("Spawning Claude with the audit prompt. This typically takes 5–15 minutes.")}`);
103
+ console.log(` ${dim("Output streams to your terminal AND lands in the report file when done.")}`);
104
+ console.log("");
105
+
106
+ // Spawn `claude -p <prompt>` against the target directory. The prompt tells
107
+ // Claude to use its Agent tool to fan out to parallel investigators.
108
+ const child = spawn("claude", ["-p", PROMPT, "--permission-mode", "acceptEdits"], {
109
+ cwd: targetDir,
110
+ stdio: ["inherit", "pipe", "inherit"],
111
+ env: process.env,
112
+ });
113
+
114
+ let captured = "";
115
+ child.stdout.on("data", (buf) => {
116
+ const chunk = buf.toString();
117
+ captured += chunk;
118
+ process.stdout.write(chunk);
119
+ });
120
+
121
+ return new Promise((resolveExit) => {
122
+ child.on("error", (err) => {
123
+ console.error(`\n ${red("Failed to spawn claude:")} ${err.message}`);
124
+ console.error(` ${dim("Is the Claude Code CLI installed and on your PATH? https://claude.ai/code")}`);
125
+ resolveExit(1);
126
+ });
127
+ child.on("close", (code) => {
128
+ try {
129
+ const header = [
130
+ `# Security audit — ${targetDir.split("/").pop()}`,
131
+ `Generated: ${new Date().toISOString()}`,
132
+ `Target: \`${targetDir}\``,
133
+ `Tool: agentdesk security-check`,
134
+ ``,
135
+ `---`,
136
+ ``,
137
+ ].join("\n");
138
+ writeFileSync(outPath, header + captured);
139
+ console.log("");
140
+ console.log(` ${green("Report written:")} ${outPath}`);
141
+ if (code !== 0) {
142
+ console.log(` ${yellow("Claude exited with code")} ${code}${dim(" — report may be partial.")}`);
143
+ }
144
+ } catch (err) {
145
+ console.error(`\n ${red("Failed to write report:")} ${err.message}`);
146
+ }
147
+ resolveExit(code === null ? 1 : code);
148
+ });
149
+ });
150
+ }
151
+
152
+ export function parseSecurityCheckArgs(args) {
153
+ let cwd = null;
154
+ let out = null;
155
+ for (let i = 0; i < args.length; i++) {
156
+ if ((args[i] === "--cwd" || args[i] === "-d") && args[i + 1]) {
157
+ cwd = args[++i];
158
+ } else if (args[i] === "--out" && args[i + 1]) {
159
+ out = args[++i];
160
+ } else if (args[i] === "--help" || args[i] === "-h") {
161
+ printHelp();
162
+ process.exit(0);
163
+ }
164
+ }
165
+ return { cwd, out };
166
+ }
167
+
168
+ function printHelp() {
169
+ console.log(`
170
+ agentdesk security-check — multi-surface security audit
171
+
172
+ Usage:
173
+ agentdesk security-check Audit the current directory
174
+ agentdesk security-check -d <dir> Audit a different directory
175
+ agentdesk security-check --out <file> Custom report output path
176
+
177
+ Spawns a Claude process scoped to the target directory. Claude surveys
178
+ the codebase, fans out to parallel investigators (server / CLI / UI /
179
+ prompts), and returns a triaged punch list with file:line citations.
180
+
181
+ Output:
182
+ Streams to your terminal as Claude works, and saves a copy to:
183
+ <target>/.agentdesk/security-audit-<timestamp>.md (by default)
184
+
185
+ Requires the Claude Code CLI on your PATH. https://claude.ai/code
186
+ `);
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.25.2",
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
  },