From fc25c05625a1a2e5f7dd9ef2e0de8fde09217430 Mon Sep 17 00:00:00 2001 From: roychandensity Date: Mon, 22 Jun 2026 15:24:11 -0700 Subject: [PATCH] Document fresh Density plugin setup --- README.md | 60 +++++++++++++++++-- plugins/density/scripts/density-core.mjs | 67 +++++++++++++--------- plugins/density/scripts/density-lib.mjs | 32 +++++++++++ plugins/density/test/density-core.test.mjs | 30 ++++++++++ 4 files changed, 158 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index e324aa1..5779e3b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ # Density Codex Plugin -This repository is a Codex marketplace for the Density plugin. +This repository is the official Codex marketplace for the Density plugin. Density lets Codex set up the local Density CLI, inspect local Density data, and answer workplace analytics questions when the installed CLI exposes chart support. ## Install +Use the official Density marketplace/catalog URL: + ```bash codex plugin marketplace add https://github.com/densityco/density-codex-plugin codex plugin add density@densityai ``` +Start a new Codex thread after installing. Codex loads plugin skills and MCP tools when a thread starts, so the current thread will not reliably see a newly installed Density plugin. + Or give Codex this: ```text Please install the Density plugin: run `codex plugin marketplace add https://github.com/densityco/density-codex-plugin` and then `codex plugin add density@densityai`. After installing, start a new thread and help me set up Density. ``` -Start a new Codex thread after installing so the Density skill and MCP tools load. - ## Try It Ask Codex: @@ -43,12 +45,62 @@ Density uses local customer data through the Density CLI. If local data or auth Setup is designed as one guided flow. After install, ask Codex to set up Density; it should run safe checks automatically and show one primary next action when auth, local data, or chart support is missing. If the current CLI does not support chart questions, Codex should say that directly instead of pretending a chart was generated. +## Repository Responsibilities + +`density-codex-plugin` is the Codex integration layer: + +- publishes the Codex marketplace/catalog entry named `densityai` +- packages the installable plugin named `density` +- provides Density skills, MCP tools, setup/onboarding wrappers, and update checks +- discovers or builds a local `density-cli` checkout, then calls the CLI for real Density work + +`density-cli` is the local data and analytics engine: + +- owns Density auth, sync, local Parquet/DuckDB storage, reports, and question/chart commands +- defines the Node.js runtime requirements for the CLI and its native dependencies +- stores local customer data under the configured Density CLI data directory +- is the source of truth for CLI capabilities such as `density capabilities --format json` + ## Marketplace Layout +There are two different manifests. They are easy to mix up, but Codex uses them for different jobs. + +The marketplace/catalog manifest tells Codex which installable plugins this marketplace exposes: + ```text .agents/plugins/marketplace.json +``` + +It exposes this plugin as: + +```text +density@densityai +``` + +The plugin package manifest describes the installable Density plugin package itself: + +```text plugins/density/.codex-plugin/plugin.json plugins/density/... ``` -This shape lets Codex install the plugin from the Git repository as a marketplace source. +This package manifest is not the marketplace URL. Use `codex plugin marketplace add https://github.com/densityco/density-codex-plugin` to register the marketplace, then `codex plugin add density@densityai` to install the package declared by `plugins/density/.codex-plugin/plugin.json`. + +## Fresh-Laptop Smoke Test + +Use this checklist when validating a new machine or a clean Codex install: + +1. Confirm Codex can see GitHub and the plugin CLI: + ```bash + codex plugin marketplace add https://github.com/densityco/density-codex-plugin + codex plugin add density@densityai + codex plugin list | grep density + ``` +2. Confirm `density@densityai` is `installed, enabled`. +3. Start a new Codex thread. +4. Ask: `Set up Density`. +5. Confirm setup reports Density CLI provenance, plugin version/update status, auth status, chart capability, and local storage readiness. +6. If setup points at a local `density-cli` checkout, use Node.js 24 before running `npm install`; `density-cli` depends on `duckdb`, which can fall back to a slow native build or fail on unsupported Node versions. +7. Complete browser auth only when setup asks for it: `density auth login`. +8. After auth, rerun `Set up Density` and verify the response has at most one primary next action. +9. Ask a starter question such as `Which rooms are busiest?` and confirm the plugin either returns a real local answer/chart or a precise unsupported-capability message. diff --git a/plugins/density/scripts/density-core.mjs b/plugins/density/scripts/density-core.mjs index 50e8ead..4dfcb02 100644 --- a/plugins/density/scripts/density-core.mjs +++ b/plugins/density/scripts/density-core.mjs @@ -98,33 +98,41 @@ export async function setup(args = {}) { let capabilities = { checked: false, chartQuestions: false, reason: 'Density CLI not found.' }; let status; + let buildError; if (cli) { - const build = await ensureDensityCliBuilt(cli); - addCheck(checks, 'density cli built', true, build.reason); - capabilities = await discoverCliCapabilities(cli, { dataDir }); - addCheck( - checks, - 'density chart capability known', - capabilities.checked, - capabilities.checked - ? (capabilities.chartQuestions ? 'chart questions supported' : 'chart questions not supported by this CLI') - : capabilities.reason - ); - addCheck( - checks, - 'fast local question answering advertised', - Boolean(capabilities.questionAnswering?.localFirst && capabilities.commands?.questionStarter), - capabilities.questionAnswering?.localFirst && capabilities.commands?.questionStarter - ? `${capabilities.questionAnswering.starterQuestionCount ?? '50+'} starter questions; target ${capabilities.questionAnswering.targetTextAnswerMs ?? 5000}ms text / ${capabilities.questionAnswering.targetChartAnswerMs ?? 10000}ms charts` - : 'CLI does not advertise the fast local utilization question contract yet.' - ); - status = await runDensity(cli, ['status'], { dataDir, allowFailure: true }); - addCheck( - checks, - 'density status runs', - status.code === 0, - status.code === 0 ? 'status completed' : oneLine(status.stderr || status.stdout) - ); + try { + const build = await ensureDensityCliBuilt(cli); + addCheck(checks, 'density cli built', true, build.reason); + } catch (error) { + buildError = error instanceof Error ? error.message : String(error); + addCheck(checks, 'density cli built', false, buildError); + } + if (!buildError) { + capabilities = await discoverCliCapabilities(cli, { dataDir }); + addCheck( + checks, + 'density chart capability known', + capabilities.checked, + capabilities.checked + ? (capabilities.chartQuestions ? 'chart questions supported' : 'chart questions not supported by this CLI') + : capabilities.reason + ); + addCheck( + checks, + 'fast local question answering advertised', + Boolean(capabilities.questionAnswering?.localFirst && capabilities.commands?.questionStarter), + capabilities.questionAnswering?.localFirst && capabilities.commands?.questionStarter + ? `${capabilities.questionAnswering.starterQuestionCount ?? '50+'} starter questions; target ${capabilities.questionAnswering.targetTextAnswerMs ?? 5000}ms text / ${capabilities.questionAnswering.targetChartAnswerMs ?? 10000}ms charts` + : 'CLI does not advertise the fast local utilization question contract yet.' + ); + status = await runDensity(cli, ['status'], { dataDir, allowFailure: true }); + addCheck( + checks, + 'density status runs', + status.code === 0, + status.code === 0 ? 'status completed' : oneLine(status.stderr || status.stdout) + ); + } } addCheck(checks, 'svg to png renderer found', Boolean(await which('rsvg-convert')), 'Optional: used for inline Codex PNG chart previews.', { optional: true }); @@ -166,7 +174,12 @@ export async function setup(args = {}) { label: 'Install or point Codex at the Density CLI.', command: 'Set DENSITY_CLI_BIN or install density on PATH.', }, - cli && status?.code !== 0 && /Token|auth|Authorization|login/i.test(status.stderr || status.stdout) && { + buildError && { + id: 'install_supported_node', + label: 'Switch to Node 24 and rebuild the Density CLI.', + command: 'Use Node.js 24, then run npm install && npm run build in the Density CLI checkout.', + }, + cli && status && status.code !== 0 && /Token|auth|Authorization|login/i.test(status.stderr || status.stdout) && { id: 'auth_login', label: 'Run Density browser auth.', tool: 'auth_login', diff --git a/plugins/density/scripts/density-lib.mjs b/plugins/density/scripts/density-lib.mjs index 6cca41d..042487c 100755 --- a/plugins/density/scripts/density-lib.mjs +++ b/plugins/density/scripts/density-lib.mjs @@ -89,6 +89,34 @@ export const run = async (command, args = [], options = {}) => { return result; }; +const REQUIRED_CLI_NODE_RANGE = '>=24 <25'; + +const parseNodeMajor = (version) => { + const match = String(version ?? '').trim().match(/^v?(\d+)\./); + return match ? Number(match[1]) : undefined; +}; + +const checkCliInstallNode = async () => { + const result = await run('node', ['--version'], { allowFailure: true, timeoutMs: 5000 }); + const version = result.stdout.trim() || result.stderr.trim(); + const major = parseNodeMajor(version); + if (result.code !== 0 || major === undefined) { + return { + ok: false, + version, + message: 'Could not determine the Node.js version used for Density CLI install.', + }; + } + if (major >= 24 && major < 25) { + return { ok: true, version }; + } + return { + ok: false, + version, + message: `Density CLI install requires Node.js ${REQUIRED_CLI_NODE_RANGE}; found ${version}. The CLI depends on duckdb, which may fall back to a slow native compile or fail when npm runs under an unsupported Node version. Switch to Node 24, then rerun setup or run npm install && npm run build in the Density CLI checkout.`, + }; +}; + const knownCliRepos = () => [ process.env.DENSITY_CLI_REPO, path.join(os.homedir(), 'dev', 'density-cli'), @@ -118,6 +146,10 @@ export const ensureDensityCliBuilt = async (cli) => { if (!cli?.repo) return { built: false, reason: 'not a local repo cli' }; const dist = path.join(cli.repo, 'dist', 'cli.js'); if (await fileExists(dist)) return { built: false, reason: 'already built' }; + const node = await checkCliInstallNode(); + if (!node.ok) { + throw new Error(node.message); + } await run('npm', ['install'], { cwd: cli.repo }); await run('npm', ['run', 'build'], { cwd: cli.repo }); return { built: true, reason: 'built local repo cli' }; diff --git a/plugins/density/test/density-core.test.mjs b/plugins/density/test/density-core.test.mjs index ee793ff..0da31a2 100644 --- a/plugins/density/test/density-core.test.mjs +++ b/plugins/density/test/density-core.test.mjs @@ -416,6 +416,36 @@ test('setup reports one configure action when no CLI is discoverable', async () }); }); +test('setup reports Node version mismatch before local CLI npm install', async () => { + await withTempEnv(async (tempDir) => { + delete process.env.DENSITY_CLI_BIN; + delete process.env.DENSITY_CLI_COMMAND; + process.env.HOME = tempDir; + + const repo = path.join(tempDir, 'density-cli'); + await mkdir(path.join(repo, 'bin'), { recursive: true }); + await writeFile(path.join(repo, 'bin', 'density.mjs'), '#!/usr/bin/env node\n'); + process.env.DENSITY_CLI_REPO = repo; + + const fakeBin = path.join(tempDir, 'bin'); + await mkdir(fakeBin, { recursive: true }); + const fakeNode = path.join(fakeBin, 'node'); + await writeFile(fakeNode, '#!/bin/sh\nprintf "v25.6.0\\n"\n'); + await chmod(fakeNode, 0o755); + process.env.PATH = `${fakeBin}${path.delimiter}${process.env.PATH ?? ''}`; + + const result = await setup({ dataDir: path.join(tempDir, 'data') }); + const buildCheck = result.checks.find((check) => check.name === 'density cli built'); + + assert.equal(result.ok, false); + assert.equal(buildCheck.ok, false); + assert.match(buildCheck.detail, /requires Node\.js >=24 <25/); + assert.match(buildCheck.detail, /duckdb/); + assert.equal(result.nextAction.id, 'install_supported_node'); + assert.equal(result.userVisiblePrimaryActions, 1); + }); +}); + test('askChart returns precise unsupported capability response', async () => { await withTempEnv(async (tempDir) => { const fakeCli = path.join(tempDir, 'density.mjs');