instabox 0.5.4 → 0.6.0

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/bin/instabox.mjs CHANGED
@@ -25,6 +25,7 @@ import { checkCli, findCli, installCli, installCommand, isOnboarding, real, runC
25
25
  import { browserCommand, callbackHandler } from '../lib/login.mjs';
26
26
  import { makeDirPrivate, sessionArgs } from '../lib/ssh.mjs';
27
27
  import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
28
+ import { agentName, track } from '../lib/telemetry.mjs';
28
29
 
29
30
  // Same variables, same order as the instabox CLI, so both talk to one server.
30
31
  const API = (
@@ -61,11 +62,22 @@ function usage() {
61
62
  --no-cli do not install or update the instabox CLI
62
63
  --no-skills do not install the instabox agent skills
63
64
  npx instabox@latest <command> run an instabox CLI command (instabox help lists them)
65
+
66
+ Usage data (steps and outcomes, never names or arguments) goes to the instabox team;
67
+ INSTABOX_NO_TELEMETRY=1 or DO_NOT_TRACK=1 turns it off.
64
68
  `);
65
69
  }
66
70
 
67
71
  // ---- instabox API ------------------------------------------------------------------------------
68
- const client = createClient(API);
72
+ const VERSION = (() => {
73
+ try {
74
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
75
+ } catch {
76
+ return 'unknown';
77
+ }
78
+ })();
79
+ const client = createClient(API, { userAgent: `instabox-npm/${VERSION}` });
80
+ let freshSignIn = false;
69
81
 
70
82
  function openBrowser(url) {
71
83
  const cmd = browserCommand(url);
@@ -210,6 +222,7 @@ async function signedIn() {
210
222
  } catch {}
211
223
  }
212
224
  const t = await signInWithInvite();
225
+ freshSignIn = true;
213
226
  saveCredentials(API, t);
214
227
  return t;
215
228
  }
@@ -396,8 +409,31 @@ function nextSteps(rows) {
396
409
  async function onboard(wantBox) {
397
410
  console.log(`\n ${bold('instabox')} ${dim('· a cloud box in seconds')}\n`);
398
411
 
412
+ const started = Date.now();
399
413
  client.use(await signedIn());
400
414
  const me = await client.call('GET', '/v1/me').catch((e) => die(`instabox did not accept the sign-in: ${e.message}`));
415
+ // outcome: ready, or quota_full; never names, paths or messages.
416
+ const report = (outcome, extra = {}) =>
417
+ track(
418
+ 'npx_onboard',
419
+ me.userId,
420
+ {
421
+ outcome,
422
+ want_box: wantBox,
423
+ fresh_sign_in: freshSignIn,
424
+ duration_ms: Date.now() - started,
425
+ no_cli: process.argv.includes('--no-cli'),
426
+ no_skills: process.argv.includes('--no-skills'),
427
+ tty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
428
+ agent: agentName(),
429
+ os: process.platform,
430
+ arch: process.arch,
431
+ node_version: process.versions.node,
432
+ npm_version: VERSION,
433
+ ...extra,
434
+ },
435
+ { server: API },
436
+ );
401
437
  console.log(
402
438
  ` ${dim('signed in as')} ${bold(me.name || me.login)}${me.login && me.name ? dim(` (${me.login})`) : ''} ${dim('· not you?')} npx instabox@latest logout`,
403
439
  );
@@ -405,6 +441,7 @@ async function onboard(wantBox) {
405
441
  const cli = ensureCli();
406
442
  if (!wantBox) {
407
443
  await reportInstalls(cli);
444
+ await report('ready');
408
445
  return nextSteps([
409
446
  ['npx instabox@latest sh', 'start a new box and SSH into it'],
410
447
  ['instabox ls', 'list your boxes'],
@@ -426,15 +463,18 @@ async function onboard(wantBox) {
426
463
  // The way out of the limit is `instabox rm`, so the CLI and the skills land before the exit.
427
464
  console.log();
428
465
  await reportInstalls(cli);
466
+ await report('quota_full');
429
467
  return quotaFull(e);
430
468
  });
431
- const ready = `ready in ${((Date.now() - t0) / 1000).toFixed(1)}s`;
469
+ const boxMs = Date.now() - t0;
470
+ const ready = `ready in ${(boxMs / 1000).toFixed(1)}s`;
432
471
  if (!tty) {
433
472
  console.log(`\n ${green('✓')} box ${bold(box.name)} ${ready}`);
434
473
  const got = await client.call('GET', '/v1/machines/' + enc(box.id)).catch(() => null);
435
474
  const note = guestUserNote(box.user, box.name, got?.guestUser);
436
475
  if (note) console.log(dim(` ${note}`));
437
476
  await reportInstalls(cli);
477
+ await report('ready', { box_ready_ms: boxMs });
438
478
  return nextSteps([
439
479
  [`instabox ssh ${box.name} -- <command>`, 'run a command in it'],
440
480
  [`instabox ssh ${box.name}`, 'open a shell in it, from a terminal'],
@@ -472,6 +512,7 @@ async function onboard(wantBox) {
472
512
  const note = guestUserNote(box.user, m.name, m.guestUser);
473
513
  if (note) console.log(dim(` ${note}`));
474
514
  await reportInstalls(cli);
515
+ await report('ready', { box_ready_ms: boxMs, ssh: true });
475
516
  console.log(dim(` type ${'exit'} to leave · stuck? press Enter, then ~.`));
476
517
  // The box's prompt is ~37 columns; narrower terminals make bash scroll the input line sideways.
477
518
  if (process.stdout.isTTY && process.stdout.columns > 0 && process.stdout.columns < 60)
package/lib/api.mjs CHANGED
@@ -12,12 +12,19 @@ export class ApiError extends Error {
12
12
 
13
13
  export function createClient(
14
14
  server,
15
- { fetch = globalThis.fetch, load = () => loadCredentials(server), save = (t) => saveCredentials(server, t) } = {},
15
+ {
16
+ fetch = globalThis.fetch,
17
+ load = () => loadCredentials(server),
18
+ save = (t) => saveCredentials(server, t),
19
+ userAgent = '',
20
+ } = {},
16
21
  ) {
17
22
  let session = null;
18
23
 
19
24
  const send = (token, method, path, body, key) => {
20
25
  const headers = { Accept: 'application/json' };
26
+ // instabox-npm/<version>: the server attributes the requests to this package.
27
+ if (userAgent) headers['User-Agent'] = userAgent;
21
28
  if (token) headers.Authorization = `Bearer ${token}`;
22
29
  if (body !== undefined) headers['Content-Type'] = 'application/json';
23
30
  if (key) headers['Idempotency-Key'] = key;
@@ -0,0 +1,97 @@
1
+ // Usage telemetry (docs/analytics.md); telemetry.json is shared with cmd/instabox/telemetry.go.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { configDir } from './credentials.mjs';
6
+
7
+ // A write-only capture key, not a secret.
8
+ export const TELEMETRY_KEY = 'phc_nPznuUD5U36v4aSQVHaV4mbKwGTpjjR8Xt9Ukb5mATfY';
9
+ export const TELEMETRY_HOST = 'https://us.i.posthog.com';
10
+ export const DEFAULT_SERVER = 'https://api.instabox.sh';
11
+ // The instabox CLI prints the same when it creates telemetry.json (cmd/instabox/telemetry.go).
12
+ const NOTICE =
13
+ 'instabox sends usage data (command names and outcomes, never arguments) to help improve it;\n' +
14
+ 'set INSTABOX_NO_TELEMETRY=1 or DO_NOT_TRACK=1 to turn it off.\n';
15
+
16
+ export function telemetryDisabled(env = process.env) {
17
+ const on = (k) =>
18
+ !['', '0', 'false'].includes(
19
+ String(env[k] ?? '')
20
+ .trim()
21
+ .toLowerCase(),
22
+ );
23
+ return on('DO_NOT_TRACK') || on('INSTABOX_NO_TELEMETRY');
24
+ }
25
+
26
+ export function agentName(env = process.env) {
27
+ if (env.CLAUDECODE) return 'claude-code';
28
+ if (env.CURSOR_TRACE_ID) return 'cursor';
29
+ if (env.CODEX_SANDBOX) return 'codex';
30
+ return '';
31
+ }
32
+
33
+ function readState(env) {
34
+ try {
35
+ const st = JSON.parse(readFileSync(join(configDir(env), 'telemetry.json'), 'utf8'));
36
+ return st?.anonymousId ? st : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function writeState(env, st) {
43
+ const dir = configDir(env);
44
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
45
+ chmodSync(dir, 0o700);
46
+ const tmp = join(dir, `.telemetry.${randomUUID()}.tmp`);
47
+ writeFileSync(tmp, JSON.stringify(st) + '\n', { mode: 0o600 });
48
+ renameSync(tmp, join(dir, 'telemetry.json'));
49
+ }
50
+
51
+ // Sends event for the account userId. Never throws; waits at most 1.5 s.
52
+ export async function track(
53
+ event,
54
+ userId,
55
+ properties,
56
+ {
57
+ server,
58
+ env = process.env,
59
+ fetch = globalThis.fetch,
60
+ key = TELEMETRY_KEY,
61
+ host = TELEMETRY_HOST,
62
+ stderr = process.stderr,
63
+ } = {},
64
+ ) {
65
+ if (!key || !userId || String(server).replace(/\/+$/, '') !== DEFAULT_SERVER || telemetryDisabled(env)) return;
66
+ try {
67
+ let st = readState(env);
68
+ if (!st) {
69
+ st = { anonymousId: randomUUID() };
70
+ writeState(env, st);
71
+ if (stderr.isTTY) stderr.write(NOTICE);
72
+ }
73
+ // Another account than the last one: a fresh anonymous ID to link.
74
+ if (st.userId && st.userId !== userId) st = { anonymousId: randomUUID() };
75
+ const id = String(userId);
76
+ const lib = { $lib: 'instabox-npm' };
77
+ const batch = [
78
+ {
79
+ event,
80
+ distinct_id: id,
81
+ uuid: randomUUID(),
82
+ timestamp: new Date().toISOString(),
83
+ properties: { ...properties, ...lib },
84
+ },
85
+ ];
86
+ const merge = st.userId !== userId || !st.merged;
87
+ if (merge)
88
+ batch.push({ event: '$identify', distinct_id: id, properties: { $anon_distinct_id: st.anonymousId, ...lib } });
89
+ const res = await fetch(host + '/batch/', {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ api_key: key, batch }),
93
+ signal: AbortSignal.timeout(1500),
94
+ });
95
+ if (res.ok && merge) writeState(env, { anonymousId: st.anonymousId, userId, merged: true });
96
+ } catch {}
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "A cloud box for your coding agent, the instabox CLI and its agent skills in one command: npx instabox@latest",
5
5
  "type": "module",
6
6
  "bin": {