npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@x-cite/zgentic-desktop

v0.37.2

Published

zgentic desktop shell (Electron) — loads the web app from the configured server. Run via `npx`, it carries its OWN Electron runtime: ~120 MB downloaded on first run and ~320 MB on disk, per machine and per package version. There is NO OS integration — no

Readme

zgentic-desktop

Electron shell for the zgentic platform (DESIGN-ELECTRON.md §3). The renderer is the existing web app loaded from the configured server URL — the shell adds local bridges (auth, tray, deep links, future bsk/palette) via a context-isolated preload; it never bundles or forks the frontend.

Install / run via npx

npx @x-cite/zgentic-desktop                      # run the shell
npx @x-cite/zgentic-desktop --help               # usage, without starting the app
npx @x-cite/zgentic-desktop zgentic://settings    # start on the settings window

Know the cost before you run it. This is shape 1 of #310: the package declares electron as a real dependency and its bin launches the compiled main process out of node_modules. Measured on Electron 43.3.0 (linux-arm64):

| | | |---|---| | the tarball itself | ~43 kB, 24 files | | first-run download | ~120 MB — the Electron runtime | | on disk afterwards | ~320 MB | | how often | once per machine and per package version |

There is also no OS integration whatsoever: no installed app icon, no Applications / Start-menu entry, no auto-update, and no code signature — macOS Gatekeeper and Windows SmartScreen see an unidentified binary. So npx here is a developer-convenience channel, not the product's installer, and it does not replace one. For an installed application, build the electron-builder artefact (see "Build"), and read "Shipping shape" for what a real distribution channel still needs.

The server URL is not a command-line flag — set it in the shell's own Settings window (or open zgentic://settings). Any other arguments are forwarded to the app, which is how deep links such as zgentic://chat/<id> reach it.

Versioning

This package's version tracks the PLATFORM version (operator rule, established for the TUI on #308; @x-cite/zgentic follows the same rule). It enters that shared line at 0.37.1 and moves with the platform from there — the next platform bump takes it to 0.37.1. The old independent [email protected] line is dead; do not restart it.

Publishing (maintainers)

Why electron is an optionalDependency and not a dependency. It has to be a runtime dependency for npx to work at all (a devDependency is never installed for a consumer), but electron-builder hard-fails the installer build when it finds electron in dependencies (app-builder-lib/out/util/packageMetadata.js: "Package "electron" is only allowed in "devDependencies"", and the ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY escape hatch covers only electron-builder itself). That check inspects dependencies alone, so optionalDependencies satisfies both paths: npm/npx installs it, electron-builder builds.

The declaration is also honest — install succeeds without the runtime, and bin/zgentic-desktop.js then exits with an actionable message instead of a stack trace (the case you get under --ignore-scripts or a blocked download). But it has a consequence that is easy to ship by accident: electron-builder treats optional deps as production ones and copies electron's 314 MB dist/ into the packaged app, shipping the runtime twice (measured: 635 MB unpacked instead of 324 MB). The !node_modules/electron/** exclusions in electron-builder.yml are what prevent that — do not remove them, and re-check the packaged size after any dependency change.

prepublishOnly runs npm run compile, so out/ is rebuilt from source before every publish. The files allowlist ships only bin, out, assets and this README — verify with npm pack --dry-run before publishing, since without an allowlist a publish would ship src/, test/ and every config file. assets/ is a runtime requirement, not decoration: src/main/main.ts loads the tray icon from <package>/assets/icon.png, so dropping it from the allowlist ships a broken tray. Publishing uses NPM_TOKEN from the environment; no token belongs in any file in this repo.

Layout

src/main/           main process (CJS, tsc → out/)
  window-manager.ts   main BrowserWindow, window-state persistence, deep-link routing
  window-state.ts     size/pos persistence + off-screen recovery (pure, tested)
  profile.ts          ServerProfile: server URL + palette hotkey/hide-on-blur (pure, tested)
                      + pruneLegacyProfileKeys(): drops the #316 oidcIssuer/clientId leftovers
  deep-link.ts        zgentic:// parsing + renderer delivery (pure, tested)
  hotkey.ts           global "Hey Agent" accelerator registration (DI, tested)
  palette-window.ts   frameless always-on-top quick-ask palette (DI, tested)
  notify-center.ts    inbox item → native Notification, click → focus + deep link (DI, tested)
  tray-badge.ts       unread-count tray title / app badge (pure, tested)
  settings-window.ts  local settings page + the ONLY sign-in surface (the only file://
                      window; strict CSP)
  tray.ts             menubar/tray icon + menu
  auth/
    cli-transport.ts  CliAuthTransport over the injected fetch: POST /auth/cli/{start,token}
    token-store.ts    safeStorage-encrypted token file (0600 fallback)
    auth-bridge.ts    sign-in orchestration: the brokered flow → platform session tokens
    paste-prompt.ts   the paste path's readCode() seam as renderable state (DI, tested)
src/client-auth/      SHARED with the TUI (#316) — see its own README for why it lives here
  pkce.ts             PKCE verifier/challenge (S256)
  loopback.ts         LoopbackServer: one-shot 127.0.0.1 listener with a timeout
  cli-signin.ts       the flow: loopback + paste paths, isLoopbackRedirectUri()
  jwt.ts              jwtExpMs() — unverified exp read, for local expiry checks only
src/preload/index.ts  contextBridge → window.zgDesktop (contextIsolation, no nodeIntegration)
src/renderer/         settings.html + settings.js — server URL, account, paste panel
                      (plain, CSP: default-src 'none')
test/                 vitest (main-process modules AND src/client-auth, which compiles here)

Bridge API (window.zgDesktop)

{
  version: string,
  server: { url(), setUrl(url) → normalized origin, probe() },
  auth:   { getSession(), signIn(mode?), signOut(), onSessionChanged(cb),
            // the paste path (#316): mode "paste" parks the flow on a code
            pastePrompt(), onPastePrompt(cb), submitCode(code), cancelSignIn(),
            copySignInUrl(), openSignInUrl() },   // act on the PENDING url only
  bsk:    { status() },            // stub until feat/browser-use
  notify: { show({title, body?, deepLink?}) → {shown}, setUnread(count) },
  palette:{ open(),                // toggle the "Hey Agent" quick-ask window
            openInWindow(chatId?), // focus the main window at a chat
            getConfig(), setHotkey(accel), setHideOnBlur(b) },
  onDeepLink(cb),                  // chat navigation + notification click-through
}

setOidcIssuer / profile() / oidcIssuer / clientId are gone (they belonged to the direct-to-IdP flow #316 deleted, and for one release they were live controls that configured nothing). profile() went with them: with the other two fields removed it returned only serverUrl, which url() already answers. pruneLegacyProfileKeys deletes the two keys from an existing installation's config.json at startup.

"Hey Agent": the global hotkey (default Cmd/Ctrl+Shift+U, configurable via palette.setHotkey → profile store) toggles a frameless, always-on-top palette window positioned at the cursor. It loads the same web app at /chat?palette=1 (compact composer + answer stream; ?palette=1 is a no-op in plain browsers). Esc hides; focus-loss hides (toggleable via palette.setHideOnBlur). zgentic://chat/<id> deep links focus the main window and route the running web app; zgentic://settings opens the shell settings window. While the main window is unfocused, new inbox items raise native notifications whose clicks focus + deep-link; the unread count rides the tray/dock badge.

The web app feature-detects window.zgDesktop?.auth and prefers the shell session (see web/src/lib/desktop-bridge.ts); plain browsers are unaffected.

Auth flow

The platform-brokered native-client flow (#316) — the gcloud shape, shared verbatim with the TUI (src/client-auth/). The shell no longer speaks OIDC to an identity provider at all, and that is the point: an IdP-only flow could sign in only identities the IdP held, so a password account on the platform could not use the desktop app. The brokered flow lands on the platform's own /login, which already carries password, Google and every other method the platform supports.

  1. POST {server}/auth/cli/start with an S256 PKCE challenge and a loopback redirect_urihttp://127.0.0.1:<random>/auth/callback, bound BEFORE the call so the port in the URL is real. The listener is one-shot, 127.0.0.1-only and times out.
  2. The returned authorize_url opens in the system browser; the human authorizes there.
  3. The platform redirects to the loopback listener with the code (+ the echoed state).
  4. POST {server}/auth/cli/token with the code and the PKCE verifier returns the platform's ordinary session + refresh tokens — there is no id token and no second POST /auth/login exchange.

Refresh rides POST {server}/auth/refresh (body variant for non-browser clients); sign-out posts POST {server}/auth/logout. Tokens live in userData/auth/session.json, encrypted via safeStorage (Keychain/DPAPI) when available, else a plaintext file with mode 0600.

The paste path (--no-launch-browser, in a GUI)

The shared flow's second path is offered too, and it is reachable on purpose rather than only as a fallback: the Settings window's Account section has "Sign in with a code…" beside "Sign in…", for a machine where the browser handoff cannot come back (a blocked loopback listener, no browser, or signing in from a different computer).

It is a state of the Settings window, not a window of its own — that window is already the shell's only sign-in surface, and the code is minted by the very server configured a few controls above it. The panel shows the authorize URL (copy it, or open it here), takes the code, and hands it to the sign-in the main process is still awaiting.

It is not the weaker path. Both paths run the same shared flow with the same S256 challenge, the same one-time code and the same expiry; the only difference is whether the code arrives over a socket or from a person. PasteCodePrompt adds no security property — it only turns "a promise the flow is awaiting" into "state a window can render and an IPC message can settle". Its local TTL (the server expiry, clamped to 30 s–15 min) is hygiene: it stops an abandoned sign-in parking a live verifier in memory. The server still refuses an expired code regardless.

Mechanics worth knowing:

  • auth.signIn("paste") picks it deliberately; signIn() stays loopback, and inside the shared flow only a listener bind failure falls back to paste — a denial, a timeout or a state mismatch is a real outcome the user must see.
  • The prompt is a main-process singleton with both a push (onPastePrompt) and a pull (pastePrompt()), because a window opened after the prompt was armed would miss the push. When a prompt is armed, the Settings window is brought up — a sign-in started from the web app has no paste UI of its own.
  • copySignInUrl() / openSignInUrl() act on the URL the main process already holds; no URL is ever passed in from the renderer, so neither is an open-redirect or arbitrary-open primitive.

When the loopback path cannot open a browser at all, the authorize URL is still copied to the clipboard and announced in a native notification, and repeated in the eventual failure message.

Dev

pnpm install
pnpm run dev        # compile + launch electron (loads DEFAULT server URL; change in Settings)
pnpm run typecheck  # tsc --noEmit gate
pnpm run test       # vitest unit tests (no electron runtime needed)

Build

pnpm run build      # tsc + electron-builder --dir → dist/linux-*-unpacked (unsigned smoke)

make desktop-build from the repo root does install + typecheck + this build. Real installers (mac dmg / win nsis / linux AppImage) are configured in electron-builder.yml; publish is disabled — drop the --dir flag and add signing secrets to produce signed installers.

Shipping shape: why npx runs Electron from node_modules (#310)

Issue #310 framed three shapes for "installable via npx". Shape 1 is what ships here: the package carries its own Electron runtime and bin/zgentic-desktop.js launches it. It was chosen because it ships now, with no infrastructure to stand up first — and the cost is stated plainly above and in the package description.

Shape 2 is the intended end state, and this is not it. There, the npm package is a tiny launcher whose bin downloads the correct signed electron-builder artefact for the host OS/arch, verifies its checksum, and runs or installs it. That is what most Electron CLIs do and what would turn npx from a convenience into an actual distribution channel. It is blocked on things this repo does not have yet:

  • artefact hosting — somewhere durable to serve per-OS/per-arch builds (GitHub Releases or a bucket), plus a version→URL manifest the launcher resolves. Today electron-builder.yml sets publish: null and CI produces unsigned --dir smoke artefacts only.
  • integrity — the launcher must verify what it downloaded (published checksums, ideally signed), because it is fetching an executable from outside the npm registry.
  • macOS code signing + notarization — Developer ID certificate plus notarytool credentials. This is the part that usually blocks a first release, and note the direction: an unsigned, un-notarized download is quarantined by Gatekeeper, making shape 2 strictly worse than shape 1 until signing exists, since shape 1's runtime arrives over npm's own path.
  • Windows Authenticode — EV/OV certificate or Azure Trusted Signing, else SmartScreen warns.
  • an update channel — shape 1 has no auto-update at all; shape 2 must decide whether the installed app updates itself or the launcher re-resolves the newest artefact on each run.

The credential names for the signing work are in "Signing notes" below. Shape 3 from the issue (install nothing, just print the download link) was not implemented.

Signing notes (not wired yet)

  • macOS: CSC_LINK/CSC_KEY_PASSWORD (Developer ID cert) + notarytool creds (APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID).
  • Windows: CSC_LINK/CSC_KEY_PASSWORD (EV/OV cert; consider Azure Trusted Signing).
  • Linux: unsigned AppImage is normal; sign the update channel instead.
  • Deep links: zgentic:// is registered at runtime (setAsDefaultProtocolClient); packaged builds should also declare the protocol in the installer (builder protocols).