@trillboards/cli 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +415 -43
- package/dist/ads-client.d.ts +146 -0
- package/dist/ads-client.js +310 -0
- package/dist/agent-hook.d.ts +64 -0
- package/dist/agent-hook.js +96 -0
- package/dist/capability.d.ts +31 -0
- package/dist/capability.js +67 -0
- package/dist/claude-settings.d.ts +64 -0
- package/dist/claude-settings.js +126 -0
- package/dist/commands/ads/disable.d.ts +10 -0
- package/dist/commands/ads/disable.js +25 -0
- package/dist/commands/ads/enable.d.ts +11 -0
- package/dist/commands/ads/enable.js +32 -0
- package/dist/commands/ads/hook.d.ts +16 -0
- package/dist/commands/ads/hook.js +90 -0
- package/dist/commands/ads/login.d.ts +21 -0
- package/dist/commands/ads/login.js +108 -0
- package/dist/commands/ads/start.d.ts +20 -0
- package/dist/commands/ads/start.js +175 -0
- package/dist/commands/code.d.ts +12 -0
- package/dist/commands/code.js +84 -0
- package/dist/commands/devices/reconcile.d.ts +10 -0
- package/dist/commands/devices/reconcile.js +94 -0
- package/dist/commands/generated/generated-csi-venue-history.d.ts +15 -0
- package/dist/commands/generated/generated-csi-venue-history.js +35 -0
- package/dist/commands/generated/generated-csi-venue-occupancy.d.ts +14 -0
- package/dist/commands/generated/generated-csi-venue-occupancy.js +32 -0
- package/dist/commands/generated/generated-federated-get-latest-model-descriptor.d.ts +19 -0
- package/dist/commands/generated/generated-federated-get-latest-model-descriptor.js +47 -0
- package/dist/commands/generated/generated-federated-get-model-weights.d.ts +19 -0
- package/dist/commands/generated/generated-federated-get-model-weights.js +47 -0
- package/dist/commands/generated/generated-federated-gradient-upload.d.ts +13 -0
- package/dist/commands/generated/generated-federated-gradient-upload.js +32 -0
- package/dist/commands/generated/generated-federated-performance-upload.d.ts +13 -0
- package/dist/commands/generated/generated-federated-performance-upload.js +32 -0
- package/dist/commands/generated/generated-federated-taste-upload.d.ts +13 -0
- package/dist/commands/generated/generated-federated-taste-upload.js +32 -0
- package/dist/commands/listen.js +9 -1
- package/dist/commands/logs/tail.js +10 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +34 -9
- package/dist/dead-time.d.ts +51 -0
- package/dist/dead-time.js +119 -0
- package/dist/format-helpers.d.ts +37 -0
- package/dist/format-helpers.js +41 -0
- package/dist/http.d.ts +8 -1
- package/dist/http.js +6 -1
- package/dist/machine-id.d.ts +12 -0
- package/dist/machine-id.js +22 -0
- package/dist/pty-wrapper.d.ts +88 -0
- package/dist/pty-wrapper.js +287 -0
- package/dist/render-runner.d.ts +48 -0
- package/dist/render-runner.js +151 -0
- package/dist/render.d.ts +82 -0
- package/dist/render.js +124 -0
- package/dist/user-auth.d.ts +57 -0
- package/dist/user-auth.js +110 -0
- package/oclif.manifest.json +1186 -301
- package/package.json +9 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { ApiClient } from '@trillboards/ads-sdk';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { buildImaPlayerUrl, fetchVirtualScreenAds, isPlayableTagUrl, newImpressionId, registerVirtualScreen, reportTextAdEvent, sendVirtualScreenHeartbeat, } from '../../ads-client.js';
|
|
6
|
+
import { updateConfig } from '../../config.js';
|
|
7
|
+
import { colorize, osc8 } from '../../format-helpers.js';
|
|
8
|
+
import { refreshUserTokenIfNeeded } from '../../user-auth.js';
|
|
9
|
+
/** Default seconds between simulated dead-time fills (overridable via --interval). */
|
|
10
|
+
const DEFAULT_INTERVAL_SECONDS = 60;
|
|
11
|
+
/** Sanctioned, vetted text/announcement ad copy shown in the TTY (the video bills via the link). */
|
|
12
|
+
const SANCTIONED_AD_LINE = 'Sponsored — your idle agent earned you a paid impression. Tap to play the video ad:';
|
|
13
|
+
export default class AdsStart extends BaseCommand {
|
|
14
|
+
static description = 'Register this machine as a virtual screen and rent its AI-coding-agent dead-time for money';
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> ads start',
|
|
17
|
+
'<%= config.bin %> ads start --once',
|
|
18
|
+
'<%= config.bin %> ads start --simulate --interval 30',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
name: Flags.string({
|
|
22
|
+
description: 'Display name for this virtual screen (defaults to "Claude Code on <hostname>")',
|
|
23
|
+
}),
|
|
24
|
+
once: Flags.boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Register, heartbeat once, serve a single ad, then exit (great for testing)',
|
|
27
|
+
}),
|
|
28
|
+
simulate: Flags.boolean({
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'Treat each interval tick as a dead-time trigger (v0 stand-in for real detection)',
|
|
31
|
+
}),
|
|
32
|
+
interval: Flags.integer({
|
|
33
|
+
description: `Seconds between dead-time fills in --simulate mode (default ${DEFAULT_INTERVAL_SECONDS})`,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
shuttingDown = false;
|
|
37
|
+
async run() {
|
|
38
|
+
const { flags } = await this.parse(AdsStart);
|
|
39
|
+
this.setParsedFlags(flags);
|
|
40
|
+
const baseUrl = this.getBaseUrl();
|
|
41
|
+
// ── Auth (user JWT, refreshed if stale) ───────────────────
|
|
42
|
+
const token = await refreshUserTokenIfNeeded(baseUrl);
|
|
43
|
+
if (!token) {
|
|
44
|
+
this.error('Not logged in. Run: trillboards ads login', { exit: 1 });
|
|
45
|
+
}
|
|
46
|
+
// ── Stable machine id (generated UUID, persisted) ─────────
|
|
47
|
+
// Imported lazily-by-value so tests can stub config; the id is durable.
|
|
48
|
+
const { getStableMachineId } = await import('../../machine-id.js');
|
|
49
|
+
const machineId = getStableMachineId();
|
|
50
|
+
const screenName = flags.name || `Claude Code on ${hostname()}`;
|
|
51
|
+
// ── Register the virtual screen ───────────────────────────
|
|
52
|
+
this.log(colorize('Trillboards agent-ads', 'bold'));
|
|
53
|
+
this.log('');
|
|
54
|
+
this.log(`Machine: ${machineId}`);
|
|
55
|
+
this.log(`Screen: ${screenName}`);
|
|
56
|
+
this.log('Registering virtual screen...');
|
|
57
|
+
let reg;
|
|
58
|
+
try {
|
|
59
|
+
reg = await registerVirtualScreen(baseUrl, token, machineId, screenName);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
63
|
+
this.error(`Registration failed: ${msg}\n\nIf your session expired, run: trillboards ads login`, { exit: 1 });
|
|
64
|
+
}
|
|
65
|
+
updateConfig({ screen_id: reg.screenId, device_id: reg.deviceId });
|
|
66
|
+
this.log(colorize(`Registered (screen ${reg.screenId}${reg.isReRegistration ? ', re-used existing' : ', new'}).`, 'green'));
|
|
67
|
+
this.log('');
|
|
68
|
+
// Reuse the SDK's ApiClient for the text-ad impression. Its reportImpression
|
|
69
|
+
// targets /v1/partner/impression which records cleanly for a partner-less
|
|
70
|
+
// virtual screen. apiBase is repointed to this run's base URL.
|
|
71
|
+
const sdk = new ApiClient(`${baseUrl.replace(/\/$/, '')}/v1/partner`);
|
|
72
|
+
const intervalMs = Math.max((flags.interval ?? DEFAULT_INTERVAL_SECONDS) * 1000, 1000);
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
this.shuttingDown = true;
|
|
75
|
+
this.log('\nStopped. Your virtual screen will go idle.');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
};
|
|
78
|
+
process.on('SIGINT', cleanup);
|
|
79
|
+
process.on('SIGTERM', cleanup);
|
|
80
|
+
// ── --once: single cycle for testing ──────────────────────
|
|
81
|
+
if (flags.once) {
|
|
82
|
+
await this.heartbeat(baseUrl, reg.screenId);
|
|
83
|
+
await this.serveOneAd(baseUrl, sdk, reg.screenId, reg.deviceId);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// ── Long loop: heartbeat each tick; on dead-time, serve an ad ──
|
|
87
|
+
this.log(colorize(`Watching for dead-time (simulate=${flags.simulate ? 'on' : 'off'}). Press Ctrl+C to stop.`, 'dim'));
|
|
88
|
+
this.log('');
|
|
89
|
+
while (!this.shuttingDown) {
|
|
90
|
+
await this.heartbeat(baseUrl, reg.screenId);
|
|
91
|
+
// v0 dead-time trigger: in --simulate, every interval tick is dead-time.
|
|
92
|
+
// Without --simulate we still heartbeat (keeping the screen live) but only
|
|
93
|
+
// serve when a real detector fires (PR2). This keeps the trigger pluggable.
|
|
94
|
+
if (flags.simulate) {
|
|
95
|
+
await this.serveOneAd(baseUrl, sdk, reg.screenId, reg.deviceId);
|
|
96
|
+
}
|
|
97
|
+
await AdsStart.sleep(intervalMs);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
static sleep(ms) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
setTimeout(resolve, ms);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async heartbeat(baseUrl, screenId) {
|
|
106
|
+
const ok = await sendVirtualScreenHeartbeat(baseUrl, screenId);
|
|
107
|
+
const stamp = colorize(new Date().toISOString(), 'dim');
|
|
108
|
+
if (ok) {
|
|
109
|
+
this.log(`${stamp} ${colorize('heartbeat', 'green')} screen ${screenId}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.log(`${stamp} ${colorize('heartbeat failed', 'yellow')} screen ${screenId} (will retry)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* One dead-time fill: fetch the web VAST tag, print the sanctioned text ad with
|
|
117
|
+
* an OSC-8 link to the billable hosted player, and fire the text-ad impression.
|
|
118
|
+
*/
|
|
119
|
+
async serveOneAd(baseUrl, sdk, screenId, deviceId) {
|
|
120
|
+
let ad;
|
|
121
|
+
try {
|
|
122
|
+
ad = await fetchVirtualScreenAds(baseUrl, screenId);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
126
|
+
this.log(`${colorize(new Date().toISOString(), 'dim')} ${colorize('ad fetch error', 'red')} ${msg}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!ad.vastTagUrl) {
|
|
130
|
+
this.log(`${colorize(new Date().toISOString(), 'dim')} ${colorize('no fill', 'yellow')} (no programmatic demand right now)`);
|
|
131
|
+
// Report the no-fill so the screen's request telemetry is complete.
|
|
132
|
+
await reportTextAdEvent(baseUrl, {
|
|
133
|
+
screenId,
|
|
134
|
+
deviceId,
|
|
135
|
+
event: 'no_fill',
|
|
136
|
+
adid: newImpressionId(),
|
|
137
|
+
variantName: ad.variantName,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const impressionId = newImpressionId();
|
|
142
|
+
const playerUrl = buildImaPlayerUrl(baseUrl, screenId, ad.vastTagUrl, deviceId, ad.variantName);
|
|
143
|
+
this.log('');
|
|
144
|
+
this.log(colorize(' ' + SANCTIONED_AD_LINE, 'bold'));
|
|
145
|
+
this.log(' ' + osc8(playerUrl, colorize('▶ Play sponsored video', 'green')));
|
|
146
|
+
this.log(' ' + colorize(playerUrl, 'dim'));
|
|
147
|
+
if (!isPlayableTagUrl(ad.vastTagUrl)) {
|
|
148
|
+
this.log(' ' +
|
|
149
|
+
colorize('(note: this tag host is not on the player allowlist; the link may show no video)', 'yellow'));
|
|
150
|
+
}
|
|
151
|
+
this.log('');
|
|
152
|
+
// Fire the text-ad impression via the SDK (records for the virtual screen),
|
|
153
|
+
// and report the programmatic request event via the public OpenRTB endpoint.
|
|
154
|
+
const [impressionOk] = await Promise.all([
|
|
155
|
+
sdk.reportImpression({
|
|
156
|
+
adid: impressionId,
|
|
157
|
+
impid: impressionId,
|
|
158
|
+
did: deviceId,
|
|
159
|
+
aid: screenId,
|
|
160
|
+
duration: 0,
|
|
161
|
+
completed: false,
|
|
162
|
+
}),
|
|
163
|
+
reportTextAdEvent(baseUrl, {
|
|
164
|
+
screenId,
|
|
165
|
+
deviceId,
|
|
166
|
+
event: 'request',
|
|
167
|
+
adid: impressionId,
|
|
168
|
+
variantName: ad.variantName,
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
this.log(`${colorize(new Date().toISOString(), 'dim')} ${colorize('served', 'green')} ${ad.variantName ?? 'programmatic'} impression=${impressionOk ? 'ok' : 'queued'}`);
|
|
172
|
+
this.log(colorize(' Open the link above to play the ad and earn the video impression.', 'dim'));
|
|
173
|
+
this.log('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a coding agent and earn from its dead-time. Runs the agent byte-transparent
|
|
4
|
+
* under a PTY; while it's "thinking", plays a text/image/VIDEO ad on a side surface
|
|
5
|
+
* (a tmux split pane on a video-capable terminal, else a spawned player window).
|
|
6
|
+
*/
|
|
7
|
+
export default class Code extends BaseCommand {
|
|
8
|
+
static description: string;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static strict: boolean;
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { fetchVirtualScreenCreative, newImpressionId, registerVirtualScreen, reportTextAdEvent, reportVirtualScreenCompletion, sendVirtualScreenHeartbeat, } from '../ads-client.js';
|
|
3
|
+
import { BaseCommand } from '../base-command.js';
|
|
4
|
+
import { capabilitiesToParam, detectSurfaceCapabilities } from '../capability.js';
|
|
5
|
+
import { updateConfig } from '../config.js';
|
|
6
|
+
import { colorize } from '../format-helpers.js';
|
|
7
|
+
import { getStableMachineId } from '../machine-id.js';
|
|
8
|
+
import { isPtyAvailable, runAgentPassthrough, runWrappedAgent } from '../pty-wrapper.js';
|
|
9
|
+
import { refreshUserTokenIfNeeded } from '../user-auth.js';
|
|
10
|
+
/** Minimum gap between ads served into dead-time (a dev preference, not a server flag). */
|
|
11
|
+
const AGENT_AD_INTERVAL_MS = 180_000;
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a coding agent and earn from its dead-time. Runs the agent byte-transparent
|
|
14
|
+
* under a PTY; while it's "thinking", plays a text/image/VIDEO ad on a side surface
|
|
15
|
+
* (a tmux split pane on a video-capable terminal, else a spawned player window).
|
|
16
|
+
*/
|
|
17
|
+
export default class Code extends BaseCommand {
|
|
18
|
+
static description = "Wrap your coding agent and earn from its dead-time (plays text/image/video ads while it thinks)";
|
|
19
|
+
static examples = ['<%= config.bin %> code', '<%= config.bin %> code claude', '<%= config.bin %> code -- codex --foo'];
|
|
20
|
+
// Pass every arg through to the wrapped agent.
|
|
21
|
+
static strict = false;
|
|
22
|
+
async run() {
|
|
23
|
+
const { argv, flags } = await this.parse(Code);
|
|
24
|
+
this.setParsedFlags(flags);
|
|
25
|
+
const baseUrl = this.getBaseUrl();
|
|
26
|
+
// The agent + its args (default: claude). `--` separates them cleanly.
|
|
27
|
+
const passthrough = argv.filter((a) => a !== '--');
|
|
28
|
+
const agent = passthrough[0] || 'claude';
|
|
29
|
+
const agentArgs = passthrough.slice(1);
|
|
30
|
+
// node-pty (the PTY the wrapper ad-serves through) is an optional native dep with no
|
|
31
|
+
// Linux prebuild — if it didn't install, `code` can't ad-wrap. Degrade BEFORE auth +
|
|
32
|
+
// registration (which are only needed to SERVE ads): run the agent verbatim so a
|
|
33
|
+
// logged-out / offline Linux dev is NEVER blocked by an auth/registration error, and
|
|
34
|
+
// point them at the text hook (`ads enable`), which works in every terminal with no
|
|
35
|
+
// native deps. (codex P2: fallback must precede auth + registration.)
|
|
36
|
+
if (!isPtyAvailable()) {
|
|
37
|
+
this.log(`${colorize('◆ Trillboards', 'yellow')} ad-serving in \`code\` needs node-pty, which isn't available on this platform — running ${agent} normally.\n` +
|
|
38
|
+
` To earn from ads in any terminal, run: ${colorize('trillboards ads enable', 'green')}`);
|
|
39
|
+
const passExit = await runAgentPassthrough(agent, agentArgs);
|
|
40
|
+
process.exit(passExit);
|
|
41
|
+
}
|
|
42
|
+
const token = await refreshUserTokenIfNeeded(baseUrl);
|
|
43
|
+
if (!token) {
|
|
44
|
+
this.error('Not logged in. Run: trillboards ads login', { exit: 1 });
|
|
45
|
+
}
|
|
46
|
+
// Register (idempotent) this machine as the developer's virtual screen.
|
|
47
|
+
const machineId = getStableMachineId();
|
|
48
|
+
let reg;
|
|
49
|
+
try {
|
|
50
|
+
reg = await registerVirtualScreen(baseUrl, token, machineId, `Coding agent on ${hostname()}`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
54
|
+
this.error(`Registration failed: ${msg}\n\nIf your session expired, run: trillboards ads login`, { exit: 1 });
|
|
55
|
+
}
|
|
56
|
+
updateConfig({ screen_id: reg.screenId, device_id: reg.deviceId });
|
|
57
|
+
const caps = detectSurfaceCapabilities();
|
|
58
|
+
const capabilitiesParam = capabilitiesToParam(caps);
|
|
59
|
+
const surfaceNote = caps.video ? 'video' : caps.image ? 'image' : 'text';
|
|
60
|
+
this.log(`${colorize('◆ Trillboards', 'green')} earning from ${agent}'s dead-time · ${surfaceNote} surface · screen ${reg.screenId.slice(0, 8)}`);
|
|
61
|
+
const exitCode = await runWrappedAgent(agent, agentArgs, {
|
|
62
|
+
caps,
|
|
63
|
+
capabilitiesParam,
|
|
64
|
+
fetchCreative: (cap) => fetchVirtualScreenCreative(baseUrl, reg.screenId, cap, reg.deviceId),
|
|
65
|
+
reportImpression: (creative) => reportTextAdEvent(baseUrl, {
|
|
66
|
+
screenId: reg.screenId,
|
|
67
|
+
deviceId: reg.deviceId,
|
|
68
|
+
event: 'request',
|
|
69
|
+
adid: creative.adId || newImpressionId(),
|
|
70
|
+
variantName: creative.variantName,
|
|
71
|
+
}),
|
|
72
|
+
// Fire the SERVER-SIGNED completion beacon the serve attached (content.complete_url)
|
|
73
|
+
// — the billable agent_direct signal (Leg B). The wrapper never builds/signs a
|
|
74
|
+
// complete itself. A Leg A (programmatic) render bills via the IMA player's signed
|
|
75
|
+
// vast_event pixels instead, so it must NOT also fire the agent_direct beacon —
|
|
76
|
+
// a creative with an imaPlayerUrl is always a Leg A render (renderCreative never
|
|
77
|
+
// falls through to Leg B for it).
|
|
78
|
+
reportCompletion: (creative) => creative.imaPlayerUrl ? Promise.resolve(undefined) : reportVirtualScreenCompletion(baseUrl, creative.completeUrl),
|
|
79
|
+
heartbeat: () => sendVirtualScreenHeartbeat(baseUrl, reg.screenId, reg.deviceId),
|
|
80
|
+
intervalMs: AGENT_AD_INTERVAL_MS,
|
|
81
|
+
});
|
|
82
|
+
process.exit(exitCode);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class DevicesReconcile extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private parseExternalIds;
|
|
10
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
import { colorize, formatCount } from '../../format-helpers.js';
|
|
5
|
+
import { apiRequest } from '../../http.js';
|
|
6
|
+
export default class DevicesReconcile extends BaseCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
file: Args.string({
|
|
9
|
+
description: 'Path to your device external IDs — a JSON array, {"external_ids":[...]}, or one ID per line',
|
|
10
|
+
required: true,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
static description = 'Reconcile your device fleet — diff your external IDs against what Trillboards has registered (matched / missing / orphan / malformed / duplicate)';
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> devices reconcile my-devices.json',
|
|
16
|
+
'<%= config.bin %> devices reconcile ids.txt --format json',
|
|
17
|
+
];
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(DevicesReconcile);
|
|
20
|
+
this.setParsedFlags(flags);
|
|
21
|
+
const apiKey = this.getApiKey();
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
this.error('Not logged in. Run: trillboards login', { exit: 1 });
|
|
24
|
+
}
|
|
25
|
+
const baseUrl = this.getBaseUrl();
|
|
26
|
+
const format = this.getFormat();
|
|
27
|
+
let content;
|
|
28
|
+
try {
|
|
29
|
+
content = readFileSync(args.file, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
33
|
+
this.error(`Could not read file: ${msg}`, { exit: 1 });
|
|
34
|
+
}
|
|
35
|
+
const externalIds = this.parseExternalIds(content);
|
|
36
|
+
if (externalIds.length === 0) {
|
|
37
|
+
this.error(`No external IDs found in ${args.file}`, { exit: 1 });
|
|
38
|
+
}
|
|
39
|
+
const data = (await apiRequest('POST', '/v1/partner/devices/reconcile', {
|
|
40
|
+
apiKey,
|
|
41
|
+
baseUrl,
|
|
42
|
+
body: { external_ids: externalIds },
|
|
43
|
+
}));
|
|
44
|
+
if (format === 'json') {
|
|
45
|
+
this.output(data);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const s = data.summary;
|
|
49
|
+
this.log('');
|
|
50
|
+
this.log(` Reconciled ${formatCount(s.requested)} requested against ${formatCount(s.registered_active)} registered`);
|
|
51
|
+
this.log('');
|
|
52
|
+
this.log(` ${colorize('matched', 'green')} ${s.matched}`);
|
|
53
|
+
this.log(` ${colorize('missing', 'yellow')} ${s.missing} (in your list, not registered)`);
|
|
54
|
+
this.log(` ${colorize('orphan', 'yellow')} ${s.orphan} (registered, not in your list)`);
|
|
55
|
+
this.log(` ${colorize('malformed', 'red')} ${s.malformed} (registered with a bad external ID)`);
|
|
56
|
+
this.log(` ${colorize('duplicate', 'red')} ${s.duplicate} (one external ID on more than one device)`);
|
|
57
|
+
this.log('');
|
|
58
|
+
if (data.missing.length > 0) {
|
|
59
|
+
const shown = data.missing.slice(0, 20).join(', ');
|
|
60
|
+
this.log(` Missing: ${shown}${data.missing.length > 20 ? ' …' : ''}`);
|
|
61
|
+
}
|
|
62
|
+
if (data.malformed.length > 0) {
|
|
63
|
+
this.log(' Malformed:');
|
|
64
|
+
for (const m of data.malformed.slice(0, 20)) {
|
|
65
|
+
this.log(` ${m.device_id} ext=${m.external_id ?? '<null>'} — ${m.reason}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (data.duplicate.length > 0) {
|
|
69
|
+
this.log(' Duplicate:');
|
|
70
|
+
for (const d of data.duplicate.slice(0, 20)) {
|
|
71
|
+
this.log(` ${d.external_id} → ${d.device_ids.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
parseExternalIds(content) {
|
|
76
|
+
const trimmed = content.trim();
|
|
77
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(trimmed);
|
|
80
|
+
const arr = Array.isArray(parsed) ? parsed : parsed.external_ids;
|
|
81
|
+
if (Array.isArray(arr)) {
|
|
82
|
+
return arr.map((x) => String(x).trim()).filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not JSON — fall through to newline parsing.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return trimmed
|
|
90
|
+
.split(/\r?\n/)
|
|
91
|
+
.map((l) => l.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Csivenuehistory extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
screenId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
hours: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// AUTO-GENERATED from OpenAPI spec (sdk-api) -- do not edit manually
|
|
2
|
+
// Regenerate with: npm run generate
|
|
3
|
+
import { Args, Flags } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { apiRequest } from '../../http.js';
|
|
6
|
+
import { formatOutput } from '../../output.js';
|
|
7
|
+
export default class Csivenuehistory extends BaseCommand {
|
|
8
|
+
static description = "Historical venue occupancy in 10-minute CSI buckets.";
|
|
9
|
+
static examples = [`$ trillboards generated csi-venue-history`];
|
|
10
|
+
static args = {
|
|
11
|
+
screenId: Args.string({ description: 'Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
'hours': Flags.string({ description: 'Look-back window in hours. Absent or non-positive values fall back to 24; values above 168 are capped to 168 (7 days).' }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(Csivenuehistory);
|
|
19
|
+
this.setParsedFlags(flags);
|
|
20
|
+
const apiKey = this.getApiKey();
|
|
21
|
+
if (!apiKey)
|
|
22
|
+
this.error('Not logged in. Run: trillboards login');
|
|
23
|
+
let urlPath = '/v2/csi/venue/{screenId}/history';
|
|
24
|
+
urlPath = urlPath.replace('{screenId}', args['screenId']);
|
|
25
|
+
const params = {};
|
|
26
|
+
if (flags['hours'])
|
|
27
|
+
params['hours'] = flags['hours'];
|
|
28
|
+
const result = await apiRequest('GET', urlPath, {
|
|
29
|
+
apiKey,
|
|
30
|
+
baseUrl: flags['base-url'] ?? 'https://api.trillboards.com',
|
|
31
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
32
|
+
});
|
|
33
|
+
this.log(formatOutput(result, this.getFormat()));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Csivenueoccupancy extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
screenId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// AUTO-GENERATED from OpenAPI spec (sdk-api) -- do not edit manually
|
|
2
|
+
// Regenerate with: npm run generate
|
|
3
|
+
import { Args } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { apiRequest } from '../../http.js';
|
|
6
|
+
import { formatOutput } from '../../output.js';
|
|
7
|
+
export default class Csivenueoccupancy extends BaseCommand {
|
|
8
|
+
static description = "Real-time venue occupancy from WiFi CSI sensing.";
|
|
9
|
+
static examples = [`$ trillboards generated csi-venue-occupancy`];
|
|
10
|
+
static args = {
|
|
11
|
+
screenId: Args.string({ description: 'Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args, flags } = await this.parse(Csivenueoccupancy);
|
|
18
|
+
this.setParsedFlags(flags);
|
|
19
|
+
const apiKey = this.getApiKey();
|
|
20
|
+
if (!apiKey)
|
|
21
|
+
this.error('Not logged in. Run: trillboards login');
|
|
22
|
+
let urlPath = '/v2/csi/venue/{screenId}/occupancy';
|
|
23
|
+
urlPath = urlPath.replace('{screenId}', args['screenId']);
|
|
24
|
+
const params = {};
|
|
25
|
+
const result = await apiRequest('GET', urlPath, {
|
|
26
|
+
apiKey,
|
|
27
|
+
baseUrl: flags['base-url'] ?? 'https://api.trillboards.com',
|
|
28
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
29
|
+
});
|
|
30
|
+
this.log(formatOutput(result, this.getFormat()));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Federatedgetlatestmodeldescriptor extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
modelType: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
venue_type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
venue_subtype: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
daypart: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
geo: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
device_profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// AUTO-GENERATED from OpenAPI spec (federated-api) -- do not edit manually
|
|
2
|
+
// Regenerate with: npm run generate
|
|
3
|
+
import { Args, Flags } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { apiRequest } from '../../http.js';
|
|
6
|
+
import { formatOutput } from '../../output.js';
|
|
7
|
+
export default class Federatedgetlatestmodeldescriptor extends BaseCommand {
|
|
8
|
+
static description = "Fetch the latest aggregated model descriptor (download URL + sha256 + version).";
|
|
9
|
+
static examples = [`$ trillboards generated federated-get-latest-model-descriptor`];
|
|
10
|
+
static args = {
|
|
11
|
+
modelType: Args.string({ description: 'Federated model name.', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
'venue_type': Flags.string({ description: 'Venue category (required).' }),
|
|
16
|
+
'venue_subtype': Flags.string({ description: 'venue_subtype' }),
|
|
17
|
+
'daypart': Flags.string({ description: 'daypart' }),
|
|
18
|
+
'geo': Flags.string({ description: 'geo' }),
|
|
19
|
+
'device_profile': Flags.string({ description: 'device_profile' }),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(Federatedgetlatestmodeldescriptor);
|
|
23
|
+
this.setParsedFlags(flags);
|
|
24
|
+
const apiKey = this.getApiKey();
|
|
25
|
+
if (!apiKey)
|
|
26
|
+
this.error('Not logged in. Run: trillboards login');
|
|
27
|
+
let urlPath = '/v1/federated/models/{modelType}/latest';
|
|
28
|
+
urlPath = urlPath.replace('{modelType}', args['modelType']);
|
|
29
|
+
const params = {};
|
|
30
|
+
if (flags['venue_type'])
|
|
31
|
+
params['venue_type'] = flags['venue_type'];
|
|
32
|
+
if (flags['venue_subtype'])
|
|
33
|
+
params['venue_subtype'] = flags['venue_subtype'];
|
|
34
|
+
if (flags['daypart'])
|
|
35
|
+
params['daypart'] = flags['daypart'];
|
|
36
|
+
if (flags['geo'])
|
|
37
|
+
params['geo'] = flags['geo'];
|
|
38
|
+
if (flags['device_profile'])
|
|
39
|
+
params['device_profile'] = flags['device_profile'];
|
|
40
|
+
const result = await apiRequest('GET', urlPath, {
|
|
41
|
+
apiKey,
|
|
42
|
+
baseUrl: flags['base-url'] ?? 'https://api.trillboards.com',
|
|
43
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
44
|
+
});
|
|
45
|
+
this.log(formatOutput(result, this.getFormat()));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Federatedgetmodelweights extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
modelType: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
venue_type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
venue_subtype: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
daypart: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
geo: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
device_profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// AUTO-GENERATED from OpenAPI spec (federated-api) -- do not edit manually
|
|
2
|
+
// Regenerate with: npm run generate
|
|
3
|
+
import { Args, Flags } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { apiRequest } from '../../http.js';
|
|
6
|
+
import { formatOutput } from '../../output.js';
|
|
7
|
+
export default class Federatedgetmodelweights extends BaseCommand {
|
|
8
|
+
static description = "Fetch aggregated weights for the device-specific slice.";
|
|
9
|
+
static examples = [`$ trillboards generated federated-get-model-weights`];
|
|
10
|
+
static args = {
|
|
11
|
+
modelType: Args.string({ description: 'Federated model name.', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
'venue_type': Flags.string({ description: 'Venue category for slice lookup. Snake_case query alias of venueType.' }),
|
|
16
|
+
'venue_subtype': Flags.string({ description: 'venue_subtype' }),
|
|
17
|
+
'daypart': Flags.string({ description: 'daypart' }),
|
|
18
|
+
'geo': Flags.string({ description: 'geo' }),
|
|
19
|
+
'device_profile': Flags.string({ description: 'device_profile' }),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(Federatedgetmodelweights);
|
|
23
|
+
this.setParsedFlags(flags);
|
|
24
|
+
const apiKey = this.getApiKey();
|
|
25
|
+
if (!apiKey)
|
|
26
|
+
this.error('Not logged in. Run: trillboards login');
|
|
27
|
+
let urlPath = '/v1/federated/models/{modelType}';
|
|
28
|
+
urlPath = urlPath.replace('{modelType}', args['modelType']);
|
|
29
|
+
const params = {};
|
|
30
|
+
if (flags['venue_type'])
|
|
31
|
+
params['venue_type'] = flags['venue_type'];
|
|
32
|
+
if (flags['venue_subtype'])
|
|
33
|
+
params['venue_subtype'] = flags['venue_subtype'];
|
|
34
|
+
if (flags['daypart'])
|
|
35
|
+
params['daypart'] = flags['daypart'];
|
|
36
|
+
if (flags['geo'])
|
|
37
|
+
params['geo'] = flags['geo'];
|
|
38
|
+
if (flags['device_profile'])
|
|
39
|
+
params['device_profile'] = flags['device_profile'];
|
|
40
|
+
const result = await apiRequest('GET', urlPath, {
|
|
41
|
+
apiKey,
|
|
42
|
+
baseUrl: flags['base-url'] ?? 'https://api.trillboards.com',
|
|
43
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
44
|
+
});
|
|
45
|
+
this.log(formatOutput(result, this.getFormat()));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class Federatedgradientupload extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {};
|
|
6
|
+
static flags: {
|
|
7
|
+
data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|