Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ on:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: node bin/agentify-desktop.mjs --help
- run: npm test
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.2.3 - 2026-05-17

### Fixed
- Fixed the Windows npm/global GUI launcher path by running Electron through its package CLI with Node instead of the Windows `.cmd` shim.
- Applied the same safer Electron launch resolution to MCP desktop auto-start.

### Changed
- Added Windows CI coverage for install/test and the npm CLI help path.
- Added README Windows notes for Chrome CDP and explicit browser executable configuration.

## 0.2.2 - 2026-05-17

### Fixed
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ Agentify Desktop does not bypass CAPTCHAs or use third-party solvers. If a verif

If your account uses Google, Microsoft, or Apple SSO, keep auth popups enabled in the Control Center. If embedded login remains unreliable, use Chrome CDP.

## Windows Notes

Use Node.js 20 or 22 on Windows. Agentify Desktop is tested against Windows in CI, including the npm CLI launcher path.

Chrome CDP is still the recommended backend on Windows because Google and Microsoft SSO can block embedded Electron login. Agentify looks for Chrome, Chromium, Brave, and Microsoft Edge in the usual install locations and on `PATH`.

If Chrome CDP cannot find your browser, set the executable explicitly:

```powershell
$env:AGENTIFY_DESKTOP_CHROME_BIN = "C:\Program Files\Google\Chrome\Application\chrome.exe"
npx @agentify/desktop
```

## Local Data And Privacy

Agentify Desktop is local-first:
Expand Down
32 changes: 27 additions & 5 deletions bin/agentify-desktop.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,36 @@ function resolveMode(invokedName, argv) {
return { mode: 'unknown', args: argv };
}

function electronBin() {
function electronLaunch() {
const override = String(process.env.AGENTIFY_DESKTOP_ELECTRON_BIN || '').trim();
if (override) {
return {
command: override,
argsPrefix: [],
shell: process.platform === 'win32' && /\.(cmd|bat)$/i.test(override)
};
}

const electronCli = path.join(packageRoot, 'node_modules', 'electron', 'cli.js');
if (fs.existsSync(electronCli)) {
return { command: process.execPath, argsPrefix: [electronCli], shell: false };
}

const local = path.join(
packageRoot,
'node_modules',
'.bin',
process.platform === 'win32' ? 'electron.cmd' : 'electron'
);
if (fs.existsSync(local)) return local;
return process.env.AGENTIFY_DESKTOP_ELECTRON_BIN || 'electron';
if (fs.existsSync(local)) {
return {
command: local,
argsPrefix: [],
shell: process.platform === 'win32'
};
}

return { command: 'electron', argsPrefix: [], shell: process.platform === 'win32' };
}

async function runMcp(args) {
Expand All @@ -58,10 +79,11 @@ async function runMcp(args) {
}

function runGui(args) {
const child = spawn(electronBin(), [packageRoot, ...args], {
const launch = electronLaunch();
const child = spawn(launch.command, [...launch.argsPrefix, packageRoot, ...args], {
stdio: 'inherit',
env: process.env,
shell: process.platform === 'win32'
shell: launch.shell
});
child.on('error', (err) => {
console.error(`agentify-desktop failed to start: ${err.message}`);
Expand Down
6 changes: 5 additions & 1 deletion context-packer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ function normalizeAbsoluteInputPath(value, { cwd = process.cwd() } = {}) {
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
}

function displayPath(filePath) {
return String(filePath || '').replace(/\\/g, '/');
}

function looksBinaryByName(filePath) {
return BINARY_EXTS.has(extnameLower(filePath));
}
Expand Down Expand Up @@ -295,7 +299,7 @@ export async function prepareQueryContext({

for (const file of files) {
const rel = roots.length ? path.relative(path.dirname(roots[0].path), file.absPath) : path.basename(file.absPath);
const named = rel && !rel.startsWith('..') ? rel : path.basename(file.absPath);
const named = displayPath(rel && !rel.startsWith('..') ? rel : path.basename(file.absPath));
if (looksBinaryByName(file.absPath)) {
if (attachedFiles.length < maxAttachmentFiles && file.size <= maxBinaryAttachmentBytes && !attachedSet.has(file.absPath)) {
attachedFiles.push({ path: file.absPath, reason: 'context-binary', size: file.size });
Expand Down
42 changes: 25 additions & 17 deletions mcp-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ async function fileExists(p) {
}
}

async function electronLaunch({ platform, allowFallback = false }) {
const override = String(process.env.AGENTIFY_DESKTOP_ELECTRON_BIN || '').trim();
if (override) {
return {
command: override,
argsPrefix: [],
shell: platform === 'win32' && /\.(cmd|bat)$/i.test(override)
};
}

const electronCli = path.resolve(__dirname, 'node_modules', 'electron', 'cli.js');
if (await fileExists(electronCli)) {
return { command: process.execPath, argsPrefix: [electronCli], shell: false };
}

if (allowFallback) {
return { command: 'electron', argsPrefix: [], shell: platform === 'win32' };
}

throw new Error('missing_electron_binary');
}

export async function loadConnection({ stateDir }) {
const state = await readState(stateDir);
const token = await readToken(stateDir);
Expand Down Expand Up @@ -80,33 +102,19 @@ export async function ensureDesktopRunning({
}
}

const defaultElectronBin = path.resolve(
__dirname,
'node_modules',
'.bin',
platform === 'win32' ? 'electron.cmd' : 'electron'
);
const entry = path.join(__dirname, 'main.mjs');
const usingCustomSpawn = spawnImpl !== spawn;
let electronBin = defaultElectronBin;
if (!(await fileExists(electronBin))) {
if (usingCustomSpawn) {
electronBin = process.env.AGENTIFY_DESKTOP_ELECTRON_BIN || 'electron';
} else {
throw new Error('missing_electron_binary');
}
}
const launch = await electronLaunch({ platform, allowFallback: spawnImpl !== spawn });
if (!(await fileExists(entry))) throw new Error('missing_desktop_entry');

spawnImpl(electronBin, [entry], {
spawnImpl(launch.command, [...launch.argsPrefix, entry], {
detached: true,
stdio: 'ignore',
env: {
...process.env,
AGENTIFY_DESKTOP_STATE_DIR: stateDir,
...(showTabs ? { AGENTIFY_DESKTOP_SHOW_TABS: 'true' } : {})
},
shell: platform === 'win32'
shell: launch.shell
})?.unref?.();

const start = Date.now();
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentify/desktop",
"version": "0.2.2",
"version": "0.2.3",
"description": "Agentify Desktop control center and MCP server for local AI web sessions",
"license": "MPL-2.0",
"type": "module",
Expand Down
10 changes: 6 additions & 4 deletions tests/bundle-store.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ test('bundle-store: ignores blank attachment/context entries', async () => {
test('bundle-store: ignores legacy relative paths when reading persisted bundles', async () => {
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentify-bundles-legacy-relative-'));
const bundleFile = path.join(stateDir, 'bundles.json');
const absFile = path.join(stateDir, 'abs.txt');
const absDir = path.join(stateDir, 'abs-dir');
await fs.writeFile(
bundleFile,
JSON.stringify(
Expand All @@ -64,8 +66,8 @@ test('bundle-store: ignores legacy relative paths when reading persisted bundles
{
name: 'legacy',
promptPrefix: 'Review carefully.',
attachments: ['./README.md', '/tmp/abs.txt'],
contextPaths: ['./src', '/tmp/abs-dir']
attachments: ['./README.md', absFile],
contextPaths: ['./src', absDir]
}
]
},
Expand All @@ -76,8 +78,8 @@ test('bundle-store: ignores legacy relative paths when reading persisted bundles
);

const got = await getBundle(stateDir, 'legacy');
assert.equal(got?.attachments.includes('/tmp/abs.txt'), true);
assert.equal(got?.contextPaths.includes('/tmp/abs-dir'), true);
assert.equal(got?.attachments.includes(absFile), true);
assert.equal(got?.contextPaths.includes(absDir), true);
assert.equal(got?.attachments.some((p) => !path.isAbsolute(p)), false);
assert.equal(got?.contextPaths.some((p) => !path.isAbsolute(p)), false);
assert.equal(got?.attachments.includes(path.resolve('./README.md')), false);
Expand Down
23 changes: 16 additions & 7 deletions tests/mcp-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,29 +105,37 @@ test('mcp-lib: ensureDesktopRunning spawns if serverId mismatches and then recov
assert.equal(conn.serverId, 'sid-new');
});

test('mcp-lib: Windows spawn uses shell for electron.cmd', async () => {
test('mcp-lib: Windows spawn uses Node-hosted Electron CLI without shell', async () => {
const dir = await tempDir();
const token = 't';
await ensureToken(dir);
await fs.writeFile(path.join(dir, 'token.txt'), `${token}\n`, 'utf8');
await writeState({ ok: true, port: 12345, serverId: 'sid-old' }, dir);

let fetchServerId = 'sid-wrong';
let sawShell = false;
let spawnedCmd = null;
let spawnedArgs = null;
let spawnShell = null;
const conn = await ensureDesktopRunning({
stateDir: dir,
fetchImpl: makeFetch({ getServerId: () => fetchServerId, acceptToken: token }),
platform: 'win32',
spawnImpl: (_cmd, _args, opts) => {
sawShell = opts?.shell === true;
spawnImpl: (cmd, args, opts) => {
spawnedCmd = cmd;
spawnedArgs = args;
spawnShell = opts?.shell;
fetchServerId = 'sid-new';
void writeState({ ok: true, port: 12345, serverId: 'sid-new' }, dir);
return { unref() {} };
},
timeoutMs: 3000
});
assert.equal(conn.serverId, 'sid-new');
assert.equal(sawShell, true);
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
assert.equal(spawnedCmd, process.execPath);
assert.equal(spawnedArgs?.[0], path.join(packageRoot, 'node_modules', 'electron', 'cli.js'));
assert.equal(spawnedArgs?.[1], path.join(packageRoot, 'main.mjs'));
assert.equal(spawnShell, false);
});

test('mcp-lib: ensureDesktopRunning resolves bundled electron relative to desktop package, not cwd', async () => {
Expand Down Expand Up @@ -157,8 +165,9 @@ test('mcp-lib: ensureDesktopRunning resolves bundled electron relative to deskto
assert.equal(conn.serverId, 'sid-new');
assert.equal(path.isAbsolute(spawnedCmd), true);
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
assert.equal(spawnedCmd, path.join(packageRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'electron.cmd' : 'electron'));
assert.equal(spawnedArgs?.[0], path.join(packageRoot, 'main.mjs'));
assert.equal(spawnedCmd, process.execPath);
assert.equal(spawnedArgs?.[0], path.join(packageRoot, 'node_modules', 'electron', 'cli.js'));
assert.equal(spawnedArgs?.[1], path.join(packageRoot, 'main.mjs'));
} finally {
process.chdir(originalCwd);
}
Expand Down
Loading