Skip to content

Commit 8b20101

Browse files
committed
feat: vehicles command, JWT cache pruning, CI workflow
1 parent d571c1a commit 8b20101

5 files changed

Lines changed: 73 additions & 7 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dimo",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"description": "Ask Claude where your car is, how the battery's doing, or what trips you took. Sets up in about a minute from the DIMO mobile app.",
55
"author": {
66
"name": "DIMO Network",

.github/workflows/test.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
- run: node --test dimo-auth.test.mjs
17+
working-directory: scripts
18+
- name: smoke-test CLI without credentials
19+
run: |
20+
set -e
21+
HOME=$(mktemp -d) node scripts/dimo-auth.mjs status | grep '"credentials":false'
22+
! node scripts/dimo-auth.mjs bogus

scripts/dimo-auth.mjs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const CACHE_PATH = join(DIMO_DIR, 'jwt-cache.json');
1414
const AUTH_BASE = process.env.DIMO_AUTH_BASE_URL || 'https://auth.dimo.zone';
1515
const EXCHANGE_URL =
1616
process.env.DIMO_TOKEN_EXCHANGE_URL || 'https://token-exchange-api.dimo.zone/v1/tokens/exchange';
17+
const IDENTITY_URL = process.env.DIMO_IDENTITY_API_URL || 'https://identity-api.dimo.zone/query';
1718
const VEHICLE_NFT = '0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF';
1819
const ALL_PRIVILEGES = [1, 2, 3, 4, 5, 6, 7, 8];
1920

@@ -82,7 +83,12 @@ function readCache() {
8283
}
8384
}
8485

86+
export function pruneExpired(vehicleJwts) {
87+
return Object.fromEntries(Object.entries(vehicleJwts || {}).filter(([, jwt]) => isLive(jwt)));
88+
}
89+
8590
function writeCache(cache) {
91+
cache.vehicleJwts = pruneExpired(cache.vehicleJwts);
8692
writeFileSync(CACHE_PATH, JSON.stringify(cache), { mode: 0o600 });
8793
chmodSync(CACHE_PATH, 0o600); // mode only applies on create; fix pre-existing files too
8894
}
@@ -235,6 +241,34 @@ function cmdSetup(argv) {
235241
console.log(JSON.stringify({ ok: true, credsPath: CREDS_PATH }));
236242
}
237243

244+
// Lists vehicles shared with the stored license, via the public Identity API.
245+
// Lives here (not as a curl in the skill) so the GraphQL quoting is done once, safely.
246+
async function cmdVehicles() {
247+
const creds = readCreds();
248+
if (!creds) fail('No credentials. Run setup first.');
249+
const query = `{ vehicles(filterBy: {privileged: "${creds.DIMO_CLIENT_ID}"}, first: 100) { nodes { tokenId definition { make model year } } } }`;
250+
const res = await fetch(IDENTITY_URL, {
251+
method: 'POST',
252+
headers: { 'Content-Type': 'application/json' },
253+
body: JSON.stringify({ query }),
254+
});
255+
const text = await res.text();
256+
let nodes;
257+
try {
258+
nodes = JSON.parse(text).data.vehicles.nodes;
259+
} catch {
260+
fail(`identity query failed: ${res.status} ${text.slice(0, 200)}`);
261+
}
262+
console.log(
263+
JSON.stringify(
264+
nodes.map((n) => ({
265+
tokenId: n.tokenId,
266+
name: [n.definition?.year, n.definition?.make, n.definition?.model].filter(Boolean).join(' '),
267+
})),
268+
),
269+
);
270+
}
271+
238272
function cmdStatus() {
239273
const creds = readCreds();
240274
const cache = readCache();
@@ -250,9 +284,10 @@ function cmdStatus() {
250284
const [, , cmd, ...rest] = process.argv;
251285
if (cmd === 'setup') cmdSetup(rest);
252286
else if (cmd === 'status') cmdStatus();
287+
else if (cmd === 'vehicles') await cmdVehicles();
253288
else if (cmd === 'vehicle-jwt')
254289
await cmdVehicleJwt(rest.find((a) => !a.startsWith('--')), { refresh: rest.includes('--refresh') });
255290
// Bare import (e.g. from the test file) leaves cmd undefined and argv[1] as the
256291
// importer's path — only error when this file was actually invoked as a CLI.
257292
else if (cmd !== undefined || process.argv[1]?.endsWith('dimo-auth.mjs'))
258-
fail('usage: dimo-auth.mjs <setup|status|vehicle-jwt [tokenId] [--refresh]>');
293+
fail('usage: dimo-auth.mjs <setup|status|vehicles|vehicle-jwt [tokenId] [--refresh]>');

scripts/dimo-auth.test.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ import {
77
isLive,
88
normalizeHex,
99
parseLackedPrivileges,
10+
pruneExpired,
1011
} from './dimo-auth.mjs';
1112

13+
test('pruneExpired drops dead JWTs, keeps live ones, tolerates empty input', () => {
14+
const now = Math.floor(Date.now() / 1000);
15+
const mk = (exp) => `e.${Buffer.from(JSON.stringify({ exp })).toString('base64url')}.s`;
16+
const pruned = pruneExpired({ 1: mk(now - 100), 2: mk(now + 3600) });
17+
assert.deepEqual(Object.keys(pruned), ['2']);
18+
assert.deepEqual(pruneExpired(undefined), {});
19+
});
20+
1221
test('normalizeHex adds 0x to raw hex, keeps prefixed, rejects junk', () => {
1322
const raw = 'a'.repeat(64);
1423
assert.equal(normalizeHex(raw, 64), `0x${raw}`);

skills/dimo/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: dimo
33
description: This skill should be used when the user asks to "connect my DIMO vehicle", "query my vehicle data", "get vehicle telemetry", "check my car's battery", "see my vehicle signals", "show my car stats", "use DIMO", "query DIMO", or invokes /dimo. Guides users from zero to querying live telemetry from a DIMO-connected vehicle — 1-minute setup from the DIMO mobile app, automatic JWT handling, and real-time signal queries.
4-
version: 0.2.1
4+
version: 0.2.2
55
allowed-tools: Bash, mcp__Claude_Preview__preview_start, mcp__Claude_Preview__preview_eval, mcp__Claude_Preview__preview_list
66
---
77

@@ -255,14 +255,14 @@ If setup fails (bad value), tell the user what was wrong and re-enable the form
255255

256256
## Phase 2: Vehicle discovery
257257

258-
Get the client ID from `status`, then query the public Identity API for vehicles shared with this license:
258+
List the vehicles shared with this license (public Identity API, no JWT needed):
259259

260260
```bash
261-
curl -s -X POST "https://identity-api.dimo.zone/query" \
262-
-H "Content-Type: application/json" \
263-
-d '{"query":"{ vehicles(filterBy: {privileged: \"<CLIENT_ID>\"}, first: 100) { nodes { tokenId definition { make model year } } } }"}'
261+
node "${CLAUDE_PLUGIN_ROOT}/scripts/dimo-auth.mjs" vehicles
264262
```
265263

264+
Returns JSON like `[{"tokenId":183644,"name":"2025 Ram 1500"}]`.
265+
266266
- **One vehicle** → use its `tokenId` silently.
267267
- **Multiple** → list them in chat (year make model + tokenId) and ask which to use.
268268
- **None** → the user hasn't shared vehicles with this license. Tell them: *open the DIMO app → Account → Advanced settings → Developer API Key → tap "Share all vehicles"*, then re-run this query. (Console fallback: the login.dimo.org sharing link from Phase 1.)

0 commit comments

Comments
 (0)