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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The normal CLI path is a plugin-managed runtime installed explicitly with the `i

The first managed runtime asset is published for `darwin-arm64`. Other platforms should use an explicit CLI until their runtime asset is published.

The default setup path prepares a fast starter preload. For broader customer-owned history, use `historical_export`; the plugin should not treat the starter preload as a limit on local data access.
The default setup path prepares a 30-day recent local preload for all locations and can continue the remaining supported local history in a background job. Agents should use `onboarding_status` to track that job and tell the user when the full supported local history is ready. For explicit separate customer-owned history exports, use `historical_export`; the plugin should not treat the recent preload as a limit on local data access.

The plugin ships one visual design contract at `plugins/density/assets/design.md`. Edit that file when a customer-specific artifact style is needed.

Expand Down
21 changes: 17 additions & 4 deletions plugins/density/mcp-server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
localDataProfile,
localUtilizationQuery,
onboardCustomer,
onboardingStatus,
repairFastQuestions,
resolveDataDir,
sensorHealthReport,
Expand Down Expand Up @@ -51,24 +52,34 @@ const tools = [
},
additionalProperties: false,
}),
tool('onboard_customer', 'Prepare a starter local customer dataset with staged setup by default; full preload sync requires explicit fullSync.', {
tool('onboard_customer', 'Prepare local Density data. Recommended full sync fetches 30 days for all locations now and can continue deeper supported history in the background.', {
type: 'object',
properties: {
dataDir: { type: 'string' },
orgId: { type: 'string', description: 'Optional organization id to select before syncing.' },
days: { type: 'number', minimum: 1, maximum: 14, description: 'Starter metrics preload window. Defaults to 14 days; windows over 7 days use hourly metrics.' },
fullSync: { type: 'boolean', description: 'Run starter preload metrics/occupancy/export phases. Defaults false.' },
days: { type: 'number', minimum: 1, maximum: 30, description: 'Recent metrics preload window. Defaults to 30 days; windows over 7 days use hourly metrics.' },
fullSync: { type: 'boolean', description: 'Run recent metrics/occupancy/export phases. Defaults false.' },
backgroundDeepSync: { type: 'boolean', description: 'After the recent preload completes, start a background job for deeper supported history. Defaults true for the recommended 30-day full sync.' },
backgroundDeepSyncDays: { type: 'number', minimum: 1, maximum: 365, description: 'Background deeper-history window. Defaults to 365 days.' },
prewarmQuestions: { type: 'boolean', description: 'After full sync, precompute starter-question answers and chart artifacts when supported. Defaults true.' },
timeoutSeconds: { type: 'number', minimum: 1, maximum: 600, description: 'Per-command timeout for explicit full sync.' },
},
additionalProperties: false,
}),
tool('historical_export', 'Export a larger customer-owned local history window to Parquet. Separate from the fast 14-day starter preload.', {
tool('onboarding_status', 'Check Density onboarding progress, including any background deeper-history sync started by onboard_customer.', {
type: 'object',
properties: {
dataDir: { type: 'string' },
},
additionalProperties: false,
}),
tool('historical_export', 'Export a larger customer-owned local history window to Parquet. Separate from the fast 30-day recent onboarding preload.', {
type: 'object',
properties: {
dataDir: { type: 'string' },
orgId: { type: 'string', description: 'Optional organization id to select before exporting.' },
days: { type: 'number', minimum: 1, maximum: 365, description: 'Historical local export window. Defaults to 90 days.' },
until: { type: 'string', description: 'Optional end of the historical export window, e.g. now or 30d. Defaults to now.' },
timeoutSeconds: { type: 'number', minimum: 1, maximum: 3600, description: 'Per-command timeout for historical export. Defaults to 600 seconds.' },
},
additionalProperties: false,
Expand Down Expand Up @@ -267,6 +278,8 @@ async function callTool(name, args) {
return jsonTool(await authLogin(args));
case 'onboard_customer':
return jsonTool(await onboardCustomer(args));
case 'onboarding_status':
return jsonTool(await onboardingStatus(args));
case 'historical_export':
return jsonTool(await historicalExport(args));
case 'create_demo_customer':
Expand Down
71 changes: 71 additions & 0 deletions plugins/density/scripts/density-background-deep-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { historicalExport } from './density-core.mjs';

const args = process.argv.slice(2);
const valueAfter = (flag) => {
const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined;
};

const statusFile = valueAfter('--status-file');
const dataDir = valueAfter('--data-dir');
const orgId = valueAfter('--org');
const days = Number(valueAfter('--days'));
const recentDays = Number(valueAfter('--recent-days'));

if (!statusFile || !dataDir || !Number.isInteger(days) || !Number.isInteger(recentDays)) {
throw new Error('Usage: density-background-deep-sync.mjs --data-dir <dir> --days <n> --recent-days <n> --status-file <file> [--org <org_id>]');
}

const nowIso = () => new Date().toISOString();

const readStatus = async () => {
try {
return JSON.parse(await readFile(statusFile, 'utf8'));
} catch {
return {};
}
};

const writeStatus = async (patch) => {
await mkdir(path.dirname(statusFile), { recursive: true });
const current = await readStatus();
const tempFile = `${statusFile}.${process.pid}.${Date.now()}.tmp`;
await writeFile(tempFile, `${JSON.stringify({
...current,
...patch,
updatedAt: nowIso(),
}, null, 2)}\n`);
await rename(tempFile, statusFile);
};

try {
await writeStatus({
status: 'running',
olderHistoryWindow: {
since: `${days}d`,
until: `${recentDays}d`,
},
});
const result = await historicalExport({
dataDir,
orgId,
days,
until: `${recentDays}d`,
});
await writeStatus({
status: result.ok ? 'complete' : 'failed',
completedAt: nowIso(),
result,
});
process.exitCode = result.ok ? 0 : 1;
} catch (error) {
await writeStatus({
status: 'failed',
completedAt: nowIso(),
error: error instanceof Error ? error.message : String(error),
});
process.exitCode = 1;
}
Loading