Skip to content
Draft
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
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
67 changes: 40 additions & 27 deletions plugins/density/scripts/density-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions plugins/density/scripts/density-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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' };
Expand Down
30 changes: 30 additions & 0 deletions plugins/density/test/density-core.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down