unbound-cli 0.9.0 → 0.9.2

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.
@@ -26,6 +26,7 @@ function register(program) {
26
26
  .requiredOption('--api-key <key>', 'User API key (for tool setup and login)')
27
27
  .requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
28
28
  .option('--domain <url>', 'Backend URL for discovery (defaults to configured backend)')
29
+ .option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
29
30
  .addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
30
31
  .addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
31
32
  .addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
@@ -46,6 +47,7 @@ For admin device enrollment via MDM, use \`unbound onboard-mdm\` instead.
46
47
 
47
48
  Examples:
48
49
  $ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
50
+ $ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> --backfill
49
51
  $ sudo unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
50
52
  `)
51
53
  .action(async (opts) => {
@@ -79,7 +81,9 @@ Examples:
79
81
 
80
82
  console.log('');
81
83
  output.info('Step 1/2: Installing tool bundle');
82
- const ok = await runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl });
84
+ const ok = await runSetupAllBundle(apiKey, {
85
+ backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
86
+ });
83
87
  if (!ok) return;
84
88
  setupSucceeded = true;
85
89
 
@@ -112,6 +116,7 @@ Examples:
112
116
  .requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
113
117
  .requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
114
118
  .option('--domain <url>', 'Backend URL for discovery (defaults to configured backend)')
119
+ .option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
115
120
  .addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
116
121
  .addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
117
122
  .addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
@@ -127,6 +132,7 @@ For end-user onboarding (non-MDM), use \`unbound onboard\` instead.
127
132
 
128
133
  Examples:
129
134
  $ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
135
+ $ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> --backfill
130
136
  `)
131
137
  .action(async (opts) => {
132
138
  let setupSucceeded = false;
@@ -148,7 +154,9 @@ Examples:
148
154
 
149
155
  console.log('');
150
156
  output.info('Step 1/2: Installing MDM tool bundle');
151
- const ok = await runMdmSetupAllBundle(opts.adminApiKey, { backendUrl, gatewayUrl });
157
+ const ok = await runMdmSetupAllBundle(opts.adminApiKey, {
158
+ backendUrl, gatewayUrl, backfill: !!opts.backfill,
159
+ });
152
160
  if (!ok) return;
153
161
  setupSucceeded = true;
154
162
 
@@ -212,20 +212,42 @@ async function runPythonScriptWindows(scriptPath, args, { capture }) {
212
212
  * own defaults. `mdm: true` skips --domain (frontend URL) since MDM scripts
213
213
  * have no browser-auth flow.
214
214
  */
215
- function buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, mdm } = {}) {
215
+ function buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, mdm, backfill } = {}) {
216
216
  let args = `--api-key ${shellEscape(apiKey)}`;
217
217
  if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
218
218
  if (gatewayUrl) args += ` --gateway-url ${shellEscape(gatewayUrl)}`;
219
219
  if (!mdm && frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
220
220
  if (clear) args += ' --clear';
221
+ if (backfill) args += ' --backfill';
221
222
  return args;
222
223
  }
223
224
 
225
+ // Backfill only applies to the hooks variants of Claude Code / Codex; gateway
226
+ // mode and Cursor have no local transcripts to seed.
227
+ function scriptSupportsBackfill(scriptPath) {
228
+ return scriptPath.includes('/hooks/') && (
229
+ scriptPath.startsWith('claude-code/') || scriptPath.startsWith('codex/')
230
+ );
231
+ }
232
+
233
+ // Surfaces an early note when --backfill was requested for a tool that can't
234
+ // use it. The setup scripts no-op safely too, but earlier user signal is better UX.
235
+ function noteBackfillUnsupported(label, scriptPath) {
236
+ if (scriptPath.startsWith('cursor/')) {
237
+ output.info(`${label} backfill is not supported (no historical transcript data on disk). Continuing without backfill for ${label}.`);
238
+ return;
239
+ }
240
+ if (scriptPath.includes('/gateway/')) {
241
+ output.info(`--backfill is not supported in gateway mode for ${label}. Continuing without backfill for ${label}.`);
242
+ return;
243
+ }
244
+ }
245
+
224
246
  /**
225
247
  * Runs a Python setup script from the setup repo with inherited stdio (live output).
226
248
  */
227
- async function runSetupScript(scriptPath, apiKey, { clear = false, backendUrl, frontendUrl, gatewayUrl } = {}) {
228
- const args = buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear });
249
+ async function runSetupScript(scriptPath, apiKey, { clear = false, backendUrl, frontendUrl, gatewayUrl, backfill = false } = {}) {
250
+ const args = buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, backfill });
229
251
  console.log('');
230
252
  if (isWindowsNative()) {
231
253
  await runPythonScriptWindows(scriptPath, args, { capture: false });
@@ -318,6 +340,7 @@ function register(program) {
318
340
  .option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
319
341
  .option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
320
342
  .option('--all', 'Set up the default bundle: Cursor, Claude Code (hooks), Codex (hooks)')
343
+ .option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
321
344
  .addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
322
345
  .addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
323
346
  .addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
@@ -349,6 +372,10 @@ Examples:
349
372
  $ unbound setup --all Set up the default bundle
350
373
  $ unbound setup --all --api-key <key> Login + set up the bundle
351
374
 
375
+ Seed historical sessions (Claude Code / Codex subscription mode only):
376
+ $ unbound setup claude-code --subscription --backfill Install hooks AND backfill local history
377
+ $ unbound setup codex --subscription --backfill Install hooks AND backfill local history
378
+
352
379
  One-step login and setup:
353
380
  $ unbound setup cursor --api-key <key> Login + set up Cursor
354
381
  $ unbound setup cursor claude-code-gateway --api-key <key>
@@ -414,10 +441,18 @@ automatically to authenticate before proceeding.
414
441
  const selectedTools = SETUP_TOOLS.filter(t => selected.includes(t.value));
415
442
  console.log('');
416
443
 
417
- const interactiveArgs = buildScriptArgs(apiKey, urlOpts);
418
- const ok = await runBatch(selectedTools, (tool) =>
419
- runScriptPiped(tool.script, interactiveArgs)
420
- );
444
+ if (opts.backfill) {
445
+ for (const tool of selectedTools) {
446
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
447
+ }
448
+ }
449
+ const ok = await runBatch(selectedTools, (tool) => {
450
+ const toolArgs = buildScriptArgs(apiKey, {
451
+ ...urlOpts,
452
+ backfill: opts.backfill && scriptSupportsBackfill(tool.script),
453
+ });
454
+ return runScriptPiped(tool.script, toolArgs);
455
+ });
421
456
  if (!ok) return;
422
457
 
423
458
  console.log('');
@@ -481,7 +516,10 @@ automatically to authenticate before proceeding.
481
516
  const toolName = tools[0];
482
517
 
483
518
  if (SETUP_TOOL_MAP[toolName]) {
484
- await runSetupScript(SETUP_TOOL_MAP[toolName].script, apiKey, { clear: opts.clear, ...urlOpts });
519
+ const { script, label } = SETUP_TOOL_MAP[toolName];
520
+ const backfill = opts.backfill && scriptSupportsBackfill(script);
521
+ if (opts.backfill && !backfill) noteBackfillUnsupported(label, script);
522
+ await runSetupScript(script, apiKey, { clear: opts.clear, backfill, ...urlOpts });
485
523
  } else if (MODE_TOOLS[toolName]) {
486
524
  const mode = MODE_TOOLS[toolName];
487
525
  if (opts.clear) {
@@ -495,7 +533,10 @@ automatically to authenticate before proceeding.
495
533
  useSubscription = choice === 'subscription';
496
534
  }
497
535
  const resolved = useSubscription ? mode.subscription : mode.gateway;
498
- await runSetupScript(SETUP_TOOL_MAP[resolved].script, apiKey, urlOpts);
536
+ const { script, label } = SETUP_TOOL_MAP[resolved];
537
+ const backfill = opts.backfill && scriptSupportsBackfill(script);
538
+ if (opts.backfill && !backfill) noteBackfillUnsupported(label, script);
539
+ await runSetupScript(script, apiKey, { ...urlOpts, backfill });
499
540
  }
500
541
  } else if (INSTRUCTION_TOOLS[toolName]) {
501
542
  output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
@@ -533,10 +574,19 @@ automatically to authenticate before proceeding.
533
574
  // Run automated tools with spinners
534
575
  if (resolvedScripts.length > 0) {
535
576
  console.log('');
536
- const args = buildScriptArgs(apiKey, { ...urlOpts, clear: opts.clear });
537
- const ok = await runBatch(resolvedScripts, (tool) =>
538
- runScriptPiped(tool.script, args)
539
- , { clear: opts.clear });
577
+ if (opts.backfill) {
578
+ for (const tool of resolvedScripts) {
579
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
580
+ }
581
+ }
582
+ const ok = await runBatch(resolvedScripts, (tool) => {
583
+ const toolArgs = buildScriptArgs(apiKey, {
584
+ ...urlOpts,
585
+ clear: opts.clear,
586
+ backfill: opts.backfill && scriptSupportsBackfill(tool.script),
587
+ });
588
+ return runScriptPiped(tool.script, toolArgs);
589
+ }, { clear: opts.clear });
540
590
  if (!ok) return;
541
591
  }
542
592
 
@@ -572,6 +622,7 @@ automatically to authenticate before proceeding.
572
622
  .requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
573
623
  .option('--clear', 'Remove Unbound configuration for the specified tools')
574
624
  .option('--all', 'Set up all available tools')
625
+ .option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
575
626
  .addHelpText('after', `
576
627
  Available tools:
577
628
  cursor Cursor IDE
@@ -590,6 +641,8 @@ Examples:
590
641
  $ sudo unbound setup mdm --admin-api-key KEY cursor claude-code-subscription codex-subscription
591
642
  $ sudo unbound setup mdm --admin-api-key KEY --all
592
643
  $ sudo unbound setup mdm --admin-api-key KEY --clear cursor codex-subscription
644
+ $ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription --backfill
645
+ Install hooks AND backfill local history
593
646
  `)
594
647
  .action(async (tools, opts, command) => {
595
648
  try {
@@ -650,16 +703,24 @@ Examples:
650
703
  const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
651
704
  console.log('');
652
705
 
653
- const args = buildScriptArgs(opts.adminApiKey, {
654
- backendUrl,
655
- gatewayUrl,
656
- clear: globalOpts.clear,
657
- mdm: true,
658
- });
706
+ if (globalOpts.backfill) {
707
+ for (const tool of resolvedTools) {
708
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
709
+ }
710
+ }
659
711
 
660
712
  const ok = await runBatch(
661
713
  resolvedTools,
662
- (tool) => runScriptPiped(tool.script, args),
714
+ (tool) => {
715
+ const toolArgs = buildScriptArgs(opts.adminApiKey, {
716
+ backendUrl,
717
+ gatewayUrl,
718
+ clear: globalOpts.clear,
719
+ mdm: true,
720
+ backfill: globalOpts.backfill && scriptSupportsBackfill(tool.script),
721
+ });
722
+ return runScriptPiped(tool.script, toolArgs);
723
+ },
663
724
  { clear: globalOpts.clear }
664
725
  );
665
726
  if (!ok) return;
@@ -678,10 +739,24 @@ Examples:
678
739
  * Assumes the caller has already ensured the user is logged in.
679
740
  * Returns true on success, false on failure.
680
741
  */
681
- async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl } = {}) {
742
+ async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl, backfill = false } = {}) {
682
743
  const resolvedTools = ALL_TOOLS.map(name => ({ name, ...SETUP_TOOL_MAP[name] }));
683
- const args = buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl });
684
- return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
744
+ if (backfill) {
745
+ for (const tool of resolvedTools) {
746
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
747
+ }
748
+ }
749
+ // Build args per-tool so --backfill only goes to tools whose script
750
+ // actually supports it (Claude Code hooks and Codex hooks). Cursor would
751
+ // print "not supported"; passing the flag to gateway-mode scripts would
752
+ // error out — `scriptSupportsBackfill` checks for both.
753
+ return runBatch(resolvedTools, (tool) => {
754
+ const args = buildScriptArgs(apiKey, {
755
+ backendUrl, frontendUrl, gatewayUrl,
756
+ backfill: backfill && scriptSupportsBackfill(tool.script),
757
+ });
758
+ return runScriptPiped(tool.script, args);
759
+ });
685
760
  }
686
761
 
687
762
  /**
@@ -689,10 +764,20 @@ async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl }
689
764
  * Caller must ensure the process is running as root.
690
765
  * Returns true on success, false on failure.
691
766
  */
692
- async function runMdmSetupAllBundle(adminApiKey, { backendUrl, gatewayUrl } = {}) {
767
+ async function runMdmSetupAllBundle(adminApiKey, { backendUrl, gatewayUrl, backfill = false } = {}) {
693
768
  const resolvedTools = MDM_ALL_TOOLS.map(name => ({ name, ...MDM_TOOLS[name] }));
694
- const args = buildScriptArgs(adminApiKey, { backendUrl, gatewayUrl, mdm: true });
695
- return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
769
+ if (backfill) {
770
+ for (const tool of resolvedTools) {
771
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
772
+ }
773
+ }
774
+ return runBatch(resolvedTools, (tool) => {
775
+ const args = buildScriptArgs(adminApiKey, {
776
+ backendUrl, gatewayUrl, mdm: true,
777
+ backfill: backfill && scriptSupportsBackfill(tool.script),
778
+ });
779
+ return runScriptPiped(tool.script, args);
780
+ });
696
781
  }
697
782
 
698
783
  module.exports = {