@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
package/dist/render.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render adapters for the `trillboards code` wrapper (pure command builders).
|
|
3
|
+
*
|
|
4
|
+
* Given the chosen creative + the surface capabilities, render it during dead-time:
|
|
5
|
+
* video → mpv in a non-modal tmux split pane where the terminal supports it,
|
|
6
|
+
* else a spawned mpv window (VS Code/Cursor/unknown)
|
|
7
|
+
* image → timg / kitten icat inline
|
|
8
|
+
* text → an OSC-8 sponsor line written to the wrapper's own output
|
|
9
|
+
*
|
|
10
|
+
* The command BUILDERS are pure (return {bin,args}) so they're unit-tested; the
|
|
11
|
+
* actual spawning lives in render-runner.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { buildSponsorMessage } from './agent-hook.js';
|
|
14
|
+
import { colorize, osc8 } from './format-helpers.js';
|
|
15
|
+
/** Sanctioned, disclosed copy for the programmatic (Leg A) video link shown in dead-time. */
|
|
16
|
+
const SANCTIONED_PROGRAMMATIC_LINE = 'Sponsored — your idle agent earned you a paid impression. Tap to play the video ad:';
|
|
17
|
+
/** mpv flags that make `--vo=kitty` play smoothly (Ghostty needs shm+really-quiet). */
|
|
18
|
+
const MPV_TERMINAL_FLAGS = [
|
|
19
|
+
'--vo=kitty',
|
|
20
|
+
'--vo-kitty-use-shm=yes',
|
|
21
|
+
'--profile=sw-fast',
|
|
22
|
+
'--really-quiet',
|
|
23
|
+
'--no-input-default-bindings',
|
|
24
|
+
'--loop=no',
|
|
25
|
+
];
|
|
26
|
+
/** POSIX single-quote a string for safe interpolation into a shell command. */
|
|
27
|
+
export function shellQuote(s) {
|
|
28
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the command to play a video in a NON-MODAL tmux split pane. `-d` keeps focus
|
|
32
|
+
* (and keystrokes) on the agent's pane — the ad plays beside it without stealing
|
|
33
|
+
* input; the pane auto-closes when mpv reaches EOF. Each tmux pane is its own grid,
|
|
34
|
+
* so the agent never repaints over the video. Requires `set -g allow-passthrough on`.
|
|
35
|
+
* `-P -F '#{pane_id}'` prints the new pane id so the runner can kill it early.
|
|
36
|
+
*/
|
|
37
|
+
export function buildTmuxPaneVideo(url, sizePct = 35) {
|
|
38
|
+
const inner = `mpv ${MPV_TERMINAL_FLAGS.join(' ')} ${shellQuote(url)}`;
|
|
39
|
+
return {
|
|
40
|
+
bin: 'tmux',
|
|
41
|
+
args: ['split-window', '-d', '-P', '-F', '#{pane_id}', '-l', `${sizePct}%`, inner],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Build the command to play a video in mpv's OWN window (fallback: VS Code/Cursor/no-tmux). */
|
|
45
|
+
export function buildSpawnWindowVideo(url, platform = process.platform) {
|
|
46
|
+
if (platform === 'darwin')
|
|
47
|
+
return { bin: 'open', args: ['-na', 'mpv', '--args', url] };
|
|
48
|
+
if (platform === 'win32')
|
|
49
|
+
return { bin: 'mpv', args: [url] };
|
|
50
|
+
// Linux/other: mpv opens its own window via the default VO.
|
|
51
|
+
return { bin: 'mpv', args: [url] };
|
|
52
|
+
}
|
|
53
|
+
/** Build the inline-image command (timg auto-detects kitty/iTerm2/sixel). */
|
|
54
|
+
export function buildImagePlayback(url) {
|
|
55
|
+
// timg auto-selects the best inline-graphics protocol; -g bounds the cell box.
|
|
56
|
+
return { bin: 'timg', args: ['-g', '60x30', '--upscale', url] };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the OS command to open a URL in the user's default browser — the Leg A webview
|
|
60
|
+
* surface (VS Code / Cursor / any no-tmux terminal). This is where AdX/IMA legitimately
|
|
61
|
+
* runs (a real DOM), and it mirrors how a Leg B video spawns its own window off-tmux.
|
|
62
|
+
*/
|
|
63
|
+
export function buildOpenUrl(url, platform = process.platform) {
|
|
64
|
+
if (platform === 'darwin')
|
|
65
|
+
return { bin: 'open', args: [url] };
|
|
66
|
+
if (platform === 'win32')
|
|
67
|
+
return { bin: 'cmd', args: ['/c', 'start', '', url] };
|
|
68
|
+
// Linux/other: xdg-open routes to the registered default browser.
|
|
69
|
+
return { bin: 'xdg-open', args: [url] };
|
|
70
|
+
}
|
|
71
|
+
/** The plain-text sponsor line (reuses the shipped `buildSponsorMessage`). */
|
|
72
|
+
export function renderTextLine(creative) {
|
|
73
|
+
const ad = {
|
|
74
|
+
adId: creative.adId,
|
|
75
|
+
name: creative.name,
|
|
76
|
+
description: creative.description,
|
|
77
|
+
campaignLink: creative.campaignLink,
|
|
78
|
+
variantName: creative.variantName,
|
|
79
|
+
};
|
|
80
|
+
return buildSponsorMessage(ad);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The programmatic (Leg A) line: sanctioned disclosure + an OSC-8 clickable link to the
|
|
84
|
+
* hosted IMA player, plus the raw URL as a fallback for terminals without OSC-8. Clicking
|
|
85
|
+
* opens the player in a browser/webview, which runs the served VAST via the Google IMA SDK
|
|
86
|
+
* (the same path the screen app uses) and bills via the wrap's signed vast_event pixels.
|
|
87
|
+
* Returns null when the creative carries no playable IMA player URL.
|
|
88
|
+
*/
|
|
89
|
+
export function renderProgrammaticLine(creative) {
|
|
90
|
+
if (!creative.imaPlayerUrl)
|
|
91
|
+
return null;
|
|
92
|
+
const link = osc8(creative.imaPlayerUrl, colorize('▶ Play sponsored video', 'green'));
|
|
93
|
+
return `${colorize(SANCTIONED_PROGRAMMATIC_LINE, 'bold')}\n ${link}\n ${colorize(creative.imaPlayerUrl, 'dim')}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Decide HOW to surface a Leg A (programmatic) fill: a clickable OSC-8 link in a non-modal
|
|
97
|
+
* tmux pane when we're in tmux, else open the hosted IMA player in the default browser.
|
|
98
|
+
* The no-tmux branch is the VS Code / Cursor / plain-terminal "webview" surface — opening a
|
|
99
|
+
* browser is the only way to run AdX/IMA there (it needs a real DOM), and it mirrors how a
|
|
100
|
+
* Leg B video spawns its own window off-tmux. So a programmatic fill is ALWAYS renderable
|
|
101
|
+
* on some surface — never dropped just because there's no tmux (the bug Codex flagged).
|
|
102
|
+
* Null only when the creative carries no playable IMA player URL.
|
|
103
|
+
*/
|
|
104
|
+
export function planProgrammaticPlayback(creative, env) {
|
|
105
|
+
if (!creative.imaPlayerUrl)
|
|
106
|
+
return null;
|
|
107
|
+
if (env.inTmux) {
|
|
108
|
+
const line = renderProgrammaticLine(creative);
|
|
109
|
+
if (!line)
|
|
110
|
+
return null;
|
|
111
|
+
return { strategy: 'tmux-link', line };
|
|
112
|
+
}
|
|
113
|
+
return { strategy: 'open-url', spec: buildOpenUrl(creative.imaPlayerUrl, env.platform ?? process.platform) };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decide HOW to play a video for this surface: a non-modal tmux split pane when we're
|
|
117
|
+
* in tmux on a video-capable terminal, else a spawned mpv window.
|
|
118
|
+
*/
|
|
119
|
+
export function planVideoPlayback(url, opts) {
|
|
120
|
+
if (opts.inTmux && opts.videoCapable) {
|
|
121
|
+
return { strategy: 'tmux-pane', spec: buildTmuxPaneVideo(url) };
|
|
122
|
+
}
|
|
123
|
+
return { strategy: 'spawn-window', spec: buildSpawnWindowVideo(url, opts.platform) };
|
|
124
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-ads user-JWT authentication.
|
|
3
|
+
*
|
|
4
|
+
* The agent-ads surface authenticates as the DEVELOPER (a Trillboards user),
|
|
5
|
+
* NOT the partner. The virtual-screen register route only accepts a user JWT
|
|
6
|
+
* (`Authorization: Bearer <JWT>` via middleware/auth.js verifyToken) — the
|
|
7
|
+
* partner `api_key` does not work there. So we maintain a separate token pair:
|
|
8
|
+
* - user_token (access JWT, 1h TTL)
|
|
9
|
+
* - user_refresh_token (refresh token, 90d TTL)
|
|
10
|
+
*
|
|
11
|
+
* Tokens are stored in ~/.trillboards/config.json (same file/perms as api_key)
|
|
12
|
+
* and are NEVER logged.
|
|
13
|
+
*/
|
|
14
|
+
/** deviceType sent to /v1/user/login + /v1/user/refresh-token (the route requires it). */
|
|
15
|
+
export declare const AGENT_CLI_DEVICE_TYPE = "agent_cli";
|
|
16
|
+
export interface LoginResult {
|
|
17
|
+
token: string;
|
|
18
|
+
refreshToken: string;
|
|
19
|
+
/** Seconds until the access token expires (server returns 3600). */
|
|
20
|
+
expiresIn: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* POST /v1/user/login {email, password, deviceType} → {token, refreshToken, expiresIn}.
|
|
24
|
+
*
|
|
25
|
+
* Throws TrillboardsApiError on non-2xx (401 invalid creds, 404 no user, etc.).
|
|
26
|
+
*/
|
|
27
|
+
export declare function loginUser(baseUrl: string, email: string, password: string): Promise<LoginResult>;
|
|
28
|
+
export interface RefreshResult {
|
|
29
|
+
/** New access token. NOTE: refresh returns `accessToken`, login returns `token`. */
|
|
30
|
+
token: string;
|
|
31
|
+
refreshToken: string;
|
|
32
|
+
expiresIn: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* POST /v1/user/refresh-token {refreshToken, deviceType} → {accessToken, refreshToken, expiresIn}.
|
|
36
|
+
*
|
|
37
|
+
* Tokens are rotated: a NEW refresh token comes back too and must be persisted.
|
|
38
|
+
*/
|
|
39
|
+
export declare function refreshUserToken(baseUrl: string, refreshToken: string): Promise<RefreshResult>;
|
|
40
|
+
/** Persist a login/refresh result to config (token, refresh token, computed expiry). */
|
|
41
|
+
export declare function persistTokens(result: {
|
|
42
|
+
token: string;
|
|
43
|
+
refreshToken: string;
|
|
44
|
+
expiresIn: number;
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the active user JWT, refreshing it first if it is missing an expiry,
|
|
48
|
+
* expired, or within the refresh skew window. Returns the valid access token, or
|
|
49
|
+
* `null` if there is no usable credential (caller should tell the user to log in).
|
|
50
|
+
*
|
|
51
|
+
* Precedence:
|
|
52
|
+
* 1. TRILLBOARDS_USER_TOKEN env (headless paste — used verbatim, never refreshed)
|
|
53
|
+
* 2. config.user_token (refreshed via config.user_refresh_token when stale)
|
|
54
|
+
*/
|
|
55
|
+
export declare function refreshUserTokenIfNeeded(baseUrl: string): Promise<string | null>;
|
|
56
|
+
/** Expose the access-token TTL for tests / callers that want to set expiry. */
|
|
57
|
+
export declare const ACCESS_TOKEN_TTL: number;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-ads user-JWT authentication.
|
|
3
|
+
*
|
|
4
|
+
* The agent-ads surface authenticates as the DEVELOPER (a Trillboards user),
|
|
5
|
+
* NOT the partner. The virtual-screen register route only accepts a user JWT
|
|
6
|
+
* (`Authorization: Bearer <JWT>` via middleware/auth.js verifyToken) — the
|
|
7
|
+
* partner `api_key` does not work there. So we maintain a separate token pair:
|
|
8
|
+
* - user_token (access JWT, 1h TTL)
|
|
9
|
+
* - user_refresh_token (refresh token, 90d TTL)
|
|
10
|
+
*
|
|
11
|
+
* Tokens are stored in ~/.trillboards/config.json (same file/perms as api_key)
|
|
12
|
+
* and are NEVER logged.
|
|
13
|
+
*/
|
|
14
|
+
import { readConfig, updateConfig } from './config.js';
|
|
15
|
+
import { TrillboardsApiError, apiRequest } from './http.js';
|
|
16
|
+
/** deviceType sent to /v1/user/login + /v1/user/refresh-token (the route requires it). */
|
|
17
|
+
export const AGENT_CLI_DEVICE_TYPE = 'agent_cli';
|
|
18
|
+
/** Access-token TTL in ms (server issues 1h tokens; expiresIn=3600). */
|
|
19
|
+
const ACCESS_TOKEN_TTL_MS = 3600 * 1000;
|
|
20
|
+
/** Refresh proactively when within this window of expiry (5 minutes). */
|
|
21
|
+
const REFRESH_SKEW_MS = 5 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* POST /v1/user/login {email, password, deviceType} → {token, refreshToken, expiresIn}.
|
|
24
|
+
*
|
|
25
|
+
* Throws TrillboardsApiError on non-2xx (401 invalid creds, 404 no user, etc.).
|
|
26
|
+
*/
|
|
27
|
+
export async function loginUser(baseUrl, email, password) {
|
|
28
|
+
const res = (await apiRequest('POST', '/v1/user/login', {
|
|
29
|
+
baseUrl,
|
|
30
|
+
body: { email, password, deviceType: AGENT_CLI_DEVICE_TYPE },
|
|
31
|
+
}));
|
|
32
|
+
const token = typeof res.token === 'string' ? res.token : '';
|
|
33
|
+
const refreshToken = typeof res.refreshToken === 'string' ? res.refreshToken : '';
|
|
34
|
+
if (!token || !refreshToken) {
|
|
35
|
+
throw new TrillboardsApiError('Login succeeded but no token was returned', 'LOGIN_NO_TOKEN', 502);
|
|
36
|
+
}
|
|
37
|
+
const expiresIn = typeof res.expiresIn === 'number' && res.expiresIn > 0 ? res.expiresIn : 3600;
|
|
38
|
+
return { token, refreshToken, expiresIn };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* POST /v1/user/refresh-token {refreshToken, deviceType} → {accessToken, refreshToken, expiresIn}.
|
|
42
|
+
*
|
|
43
|
+
* Tokens are rotated: a NEW refresh token comes back too and must be persisted.
|
|
44
|
+
*/
|
|
45
|
+
export async function refreshUserToken(baseUrl, refreshToken) {
|
|
46
|
+
const res = (await apiRequest('POST', '/v1/user/refresh-token', {
|
|
47
|
+
baseUrl,
|
|
48
|
+
body: { refreshToken, deviceType: AGENT_CLI_DEVICE_TYPE },
|
|
49
|
+
}));
|
|
50
|
+
// Refresh response uses `accessToken` (asymmetric vs login's `token`).
|
|
51
|
+
const token = typeof res.accessToken === 'string' ? res.accessToken : '';
|
|
52
|
+
const newRefresh = typeof res.refreshToken === 'string' ? res.refreshToken : '';
|
|
53
|
+
if (!token) {
|
|
54
|
+
throw new TrillboardsApiError('Refresh succeeded but no access token was returned', 'REFRESH_NO_TOKEN', 502);
|
|
55
|
+
}
|
|
56
|
+
const expiresIn = typeof res.expiresIn === 'number' && res.expiresIn > 0 ? res.expiresIn : 3600;
|
|
57
|
+
// Keep the existing refresh token if the server somehow omitted a rotated one.
|
|
58
|
+
return { token, refreshToken: newRefresh || refreshToken, expiresIn };
|
|
59
|
+
}
|
|
60
|
+
/** Persist a login/refresh result to config (token, refresh token, computed expiry). */
|
|
61
|
+
export function persistTokens(result) {
|
|
62
|
+
updateConfig({
|
|
63
|
+
user_token: result.token,
|
|
64
|
+
user_refresh_token: result.refreshToken,
|
|
65
|
+
user_token_expires_at: Date.now() + result.expiresIn * 1000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the active user JWT, refreshing it first if it is missing an expiry,
|
|
70
|
+
* expired, or within the refresh skew window. Returns the valid access token, or
|
|
71
|
+
* `null` if there is no usable credential (caller should tell the user to log in).
|
|
72
|
+
*
|
|
73
|
+
* Precedence:
|
|
74
|
+
* 1. TRILLBOARDS_USER_TOKEN env (headless paste — used verbatim, never refreshed)
|
|
75
|
+
* 2. config.user_token (refreshed via config.user_refresh_token when stale)
|
|
76
|
+
*/
|
|
77
|
+
export async function refreshUserTokenIfNeeded(baseUrl) {
|
|
78
|
+
const envToken = process.env.TRILLBOARDS_USER_TOKEN;
|
|
79
|
+
if (envToken && envToken.trim()) {
|
|
80
|
+
// Caller supplied a raw JWT; we have no refresh token for it, so use as-is.
|
|
81
|
+
return envToken.trim();
|
|
82
|
+
}
|
|
83
|
+
const cfg = readConfig();
|
|
84
|
+
const token = cfg.user_token;
|
|
85
|
+
const refreshToken = cfg.user_refresh_token;
|
|
86
|
+
if (!token)
|
|
87
|
+
return null;
|
|
88
|
+
const expiresAt = cfg.user_token_expires_at;
|
|
89
|
+
const nearExpiry = typeof expiresAt !== 'number' || Number.isNaN(expiresAt) || Date.now() >= expiresAt - REFRESH_SKEW_MS;
|
|
90
|
+
if (!nearExpiry) {
|
|
91
|
+
return token;
|
|
92
|
+
}
|
|
93
|
+
// Token is stale/near-expiry. Refresh if we can; otherwise return what we have
|
|
94
|
+
// and let the downstream 401 surface a clear re-login prompt.
|
|
95
|
+
if (!refreshToken)
|
|
96
|
+
return token;
|
|
97
|
+
try {
|
|
98
|
+
const refreshed = await refreshUserToken(baseUrl, refreshToken);
|
|
99
|
+
persistTokens(refreshed);
|
|
100
|
+
return refreshed.token;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Refresh failed (expired/rotated/revoked refresh token). Fall back to the
|
|
104
|
+
// current access token; if it is also dead the API call will 401 and the
|
|
105
|
+
// command will instruct the user to run `trillboards ads login`.
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Expose the access-token TTL for tests / callers that want to set expiry. */
|
|
110
|
+
export const ACCESS_TOKEN_TTL = ACCESS_TOKEN_TTL_MS;
|