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
8 changes: 8 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ The packaged host payload is written to `release/`.

## 2. Install the collector

For the published prebuilt host package:

```sh
npx claudething@latest install --host-name desk-mac
```

The npm command installs host-side software only and never flashes firmware. When working from a verified source checkout, the equivalent command is:

```sh
node release/install/install.mjs --host-name desk-mac
```
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<p align="center">
<a href="https://github.com/petehottelet/ClaudeThing/actions/workflows/ci.yml"><img src="https://github.com/petehottelet/ClaudeThing/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" /></a>
<a href="https://www.npmjs.com/package/claudething"><img src="https://img.shields.io/npm/v/claudething?color=d97757&logo=npm" alt="ClaudeThing npm package" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3f9e63" alt="MIT license" /></a>
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%E2%89%A520.19-5fa04e?logo=nodedotjs&logoColor=white" alt="Node.js 20.19 or newer" /></a>
<img src="https://img.shields.io/badge/status-hardware%20validated-d97757" alt="hardware validated" />
Expand Down Expand Up @@ -58,6 +59,14 @@ The easiest route is to point your coding agent at [this repository](https://git

The repository gives the agent concrete checks and safety stops for each stage. It can inspect compatibility, install the host collector, build and verify firmware, configure the dashboard, qualify the USB connection, and provision the device after an approved flash. Prefer doing it yourself? The same complete path is in [INSTALL.md](INSTALL.md).

Already running ClaudeThing firmware? Install or upgrade the prebuilt host collector from npm:

```sh
npx claudething@latest install
```

This command configures the local collector and startup service; it never flashes the device. Firmware images remain deliberately separate under [GitHub Releases](https://github.com/petehottelet/ClaudeThing/releases).

## See it in action

| Dashboard picker | AI usage by day |
Expand Down
71 changes: 71 additions & 0 deletions bin/claudething.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node

import { spawn } from "node:child_process";
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const [command, ...args] = process.argv.slice(2);

function usage() {
console.log(`ClaudeThing.ai host tools

Usage:
claudething install [options] Install or upgrade the host collector
claudething uninstall [options] Remove the host collector
claudething doctor [options] Check a USB-connected ClaudeThing display
claudething version Print the installed package version
claudething help Show this help

The npm package installs the host-side software. It never flashes firmware.
Firmware images and the guarded flashing runbook are published at:
https://github.com/petehottelet/ClaudeThing/releases`);
}

async function runNode(script, scriptArgs) {
const child = spawn(process.execPath, [script, ...scriptArgs], {
stdio: "inherit",
windowsHide: true,
});
child.on("error", (error) => {
console.error(`Unable to start ClaudeThing: ${error.message}`);
process.exitCode = 1;
});
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exitCode = code ?? 1;
});
}

switch (command) {
case undefined:
case "help":
case "--help":
case "-h":
usage();
break;
case "version":
case "--version":
case "-v": {
const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
console.log(manifest.version);
break;
}
case "install":
await runNode(resolve(packageRoot, "release/install/install.mjs"), args);
break;
case "uninstall":
await runNode(resolve(packageRoot, "release/install/uninstall.mjs"), args);
break;
case "doctor":
await runNode(resolve(packageRoot, "release/device/device-tool.mjs"), ["doctor", ...args]);
break;
default:
console.error(`Unknown command: ${command}\n`);
usage();
process.exitCode = 1;
}
7 changes: 5 additions & 2 deletions package-lock.json

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

45 changes: 42 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
{
"name": "carthing-usage-dashboard",
"private": true,
"name": "claudething",
"version": "1.1.0",
"description": "Local-first host collector and installer for the ClaudeThing.ai Spotify Car Thing dashboard.",
"license": "MIT",
"author": "Pete Hottelet",
"homepage": "https://github.com/petehottelet/ClaudeThing#readme",
"bugs": {
"url": "https://github.com/petehottelet/ClaudeThing/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/petehottelet/ClaudeThing.git"
},
"keywords": [
"spotify-car-thing",
"car-thing",
"dashboard",
"claude",
"claude-code",
"codex",
"ai-usage",
"analytics",
"local-first",
"collector"
],
"bin": {
"claudething": "bin/claudething.mjs"
},
"files": [
"bin/claudething.mjs",
"release/collector",
"release/device-ui",
"release/install",
"release/device",
"INSTALL.md",
"SECURITY.md",
"THIRD_PARTY_NOTICES.md"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=20.19"
},
Expand All @@ -21,8 +58,10 @@
"firmware:assets": "node firmware/scripts/generate-splash.mjs",
"firmware:build": "node firmware/scripts/build.mjs",
"firmware:verify": "node firmware/scripts/verify.mjs",
"verify": "node tools/provider-catalog.mjs && npm run typecheck && npm test && npm run package && npm run verify:release && npm run firmware:verify",
"verify": "node tools/provider-catalog.mjs && npm run typecheck && npm test && npm run package && npm run verify:release && npm run verify:npm && npm run firmware:verify",
"verify:release": "node tools/install-smoke.mjs && node tools/e2e-shot.mjs .audit-e2e && node tools/e2e-firmware-topology.mjs",
"verify:npm": "node tools/npm-package-smoke.mjs",
"prepack": "npm run package",
"install:local": "node install/install.mjs",
"test": "npm test --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present"
Expand Down
78 changes: 78 additions & 0 deletions tools/npm-package-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const temporary = await mkdtemp(join(tmpdir(), "claudething-npm-"));

function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
windowsHide: true,
...options,
});
if (result.status !== 0) {
throw new Error(
`${command} ${args.join(" ")} failed:\n${result.stdout ?? ""}${result.stderr ?? ""}`,
);
}
return result.stdout;
}

try {
const packed = JSON.parse(
run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", temporary]),
);
const artifact = packed[0];
if (artifact?.name !== "claudething" || artifact?.version !== "1.1.0") {
throw new Error(`Unexpected package identity: ${JSON.stringify(artifact)}`);
}

const paths = new Set(artifact.files.map((entry) => entry.path));
const required = [
"bin/claudething.mjs",
"release/collector/collector.cjs",
"release/device-ui/index.html",
"release/install/install.mjs",
"release/install/uninstall.mjs",
"release/device/device-tool.mjs",
"LICENSE",
"README.md",
"package.json",
];
for (const path of required) {
if (!paths.has(path)) throw new Error(`npm package is missing ${path}`);
}

const forbidden = [...paths].filter((path) =>
/(?:^|\/)(?:pairing\.token|dashboard-config\.jsonc)$/.test(path) ||
path.startsWith("firmware/build/") ||
path.endsWith("-flashthing.zip"),
);
if (forbidden.length > 0) {
throw new Error(`npm package contains forbidden runtime/build data: ${forbidden.join(", ")}`);
}

const prefix = join(temporary, "installed");
const tarball = join(temporary, artifact.filename);
run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--prefix", prefix, tarball]);
const cli = join(prefix, "node_modules", "claudething", "bin", "claudething.mjs");
const help = run(process.execPath, [cli, "--help"]);
if (!help.includes("never flashes firmware")) throw new Error("Installed CLI help lost the firmware safety boundary.");
const installHelp = run(process.execPath, [cli, "install", "--help"]);
if (!installHelp.includes("--pairing-token-file")) {
throw new Error("Installed CLI cannot reach the packaged host installer.");
}
const version = run(process.execPath, [cli, "--version"]).trim();
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
if (version !== manifest.version) throw new Error(`Installed CLI reported ${version}, expected ${manifest.version}.`);

console.log(
`npm package: ${artifact.filename}, ${artifact.size} bytes compressed, ${artifact.files.length} files; installed CLI smoke passed`,
);
} finally {
await rm(temporary, { recursive: true, force: true });
}