forkit-connect 0.1.0 → 0.1.1

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/dist/cli.js CHANGED
@@ -38,6 +38,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
38
38
  draftPassports: 0,
39
39
  maxWorkspaces: 0,
40
40
  maxProjects: 0,
41
+ maxGovernedPassports: 0,
41
42
  runtimeSignalsPerMonth: null,
42
43
  },
43
44
  signal: {
@@ -45,6 +46,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
45
46
  draftPassports: 25,
46
47
  maxWorkspaces: 3,
47
48
  maxProjects: 3,
49
+ maxGovernedPassports: 3,
48
50
  runtimeSignalsPerMonth: 10000,
49
51
  },
50
52
  protocol: {
@@ -52,6 +54,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
52
54
  draftPassports: 20,
53
55
  maxWorkspaces: null,
54
56
  maxProjects: null,
57
+ maxGovernedPassports: null,
55
58
  runtimeSignalsPerMonth: 250000,
56
59
  },
57
60
  sovereign: {
@@ -59,6 +62,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
59
62
  draftPassports: null,
60
63
  maxWorkspaces: null,
61
64
  maxProjects: null,
65
+ maxGovernedPassports: null,
62
66
  runtimeSignalsPerMonth: null,
63
67
  },
64
68
  };
@@ -151,6 +155,13 @@ function advancedUsage() {
151
155
  console.log(' --heartbeat-gaid <gaid> Queue heartbeat runtime signal event for GAID');
152
156
  console.log(' --heartbeat-key <key> API key used for heartbeat runtime signal event');
153
157
  console.log(' Also used by: c2 set-key (stores key + backfills events)');
158
+ console.log(' --gaid <gaid> Passport GAID used by c2 run-log emit');
159
+ console.log(' --api-key <key> Runtime signal API key used by c2 run-log emit');
160
+ console.log(' --provider <name> Provider label used by c2 run-log emit');
161
+ console.log(' --service-name <name> Service/agent/workflow label used by c2 run-log emit');
162
+ console.log(' --prompt-tokens <n> Prompt tokens used by c2 run-log emit');
163
+ console.log(' --completion-tokens <n> Completion tokens used by c2 run-log emit');
164
+ console.log(' --draft-only Allow draft creation even when governed publish capacity is full');
154
165
  }
155
166
  function showUsage() {
156
167
  if (hasFlag('--advanced-help')) {
@@ -169,6 +180,13 @@ function getArg(flag) {
169
180
  function hasFlag(flag) {
170
181
  return process.argv.slice(2).includes(flag);
171
182
  }
183
+ function getNumericArg(flag) {
184
+ const value = getArg(flag);
185
+ if (value === null)
186
+ return undefined;
187
+ const parsed = Number(value);
188
+ return Number.isFinite(parsed) ? parsed : undefined;
189
+ }
172
190
  function isHelpCommand(command) {
173
191
  return command === 'help' || command === '--help' || command === '-h' || command === '--advanced-help';
174
192
  }
@@ -182,6 +200,20 @@ function sleep(ms) {
182
200
  delay(resolve, ms);
183
201
  });
184
202
  }
203
+ async function withTimeout(promise, timeoutMs, fallbackValue) {
204
+ return new Promise((resolve) => {
205
+ const timeout = setTimeout(() => resolve(fallbackValue), Math.max(0, timeoutMs));
206
+ promise
207
+ .then((value) => {
208
+ clearTimeout(timeout);
209
+ resolve(value);
210
+ })
211
+ .catch(() => {
212
+ clearTimeout(timeout);
213
+ resolve(fallbackValue);
214
+ });
215
+ });
216
+ }
185
217
  function isDeviceConnectStartResponse(body) {
186
218
  if (!body || typeof body !== 'object')
187
219
  return false;
@@ -220,12 +252,11 @@ function printDeviceLoginInstructions(start) {
220
252
  console.log('[forkit-connect] Keep this terminal open while Forkit Connect waits for approval.');
221
253
  }
222
254
  function printSessionExportFallback(token) {
255
+ void token;
223
256
  console.log('[forkit-connect] Approval succeeded, but secure credential storage is unavailable in this Linux session.');
224
257
  console.log('[forkit-connect] Forkit Connect can keep using this approved session in the current interactive run.');
225
- console.log('[forkit-connect] To keep working in this terminal right now, export the session reference manually:');
226
- console.log(`export FORKIT_CONNECT_SESSION_REF='${token}'`);
227
- console.log('[forkit-connect] Then rerun: forkit-connect status');
228
- console.log('[forkit-connect] For a persistent and safer setup, install libsecret-tools and run forkit-connect login again.');
258
+ console.log('[forkit-connect] For persistent login, install libsecret-tools and run forkit-connect login again.');
259
+ console.log('[forkit-connect] Headless automation may pass FORKIT_CONNECT_SESSION_REF explicitly, but Connect will not print session tokens.');
229
260
  }
230
261
  function hasLinuxGuiSession() {
231
262
  if (process.platform !== 'linux') {
@@ -300,6 +331,9 @@ function formatRemainingLimit(limit, used, singular, plural = `${singular}s`) {
300
331
  const remaining = Math.max(safeLimit - safeUsed, 0);
301
332
  return `${remaining} left (${safeUsed}/${safeLimit} used)`;
302
333
  }
334
+ function isCapacityExhausted(limit, remaining) {
335
+ return Number.isFinite(limit) && Number(remaining) <= 0;
336
+ }
303
337
  function formatWorkspaceAccessLine(workspace) {
304
338
  const workspaceId = String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown');
305
339
  return `- ${summarizeWorkspaceLabel(workspace)} | id=${workspaceId}`;
@@ -577,6 +611,11 @@ function printJson(value) {
577
611
  console.log(JSON.stringify(value, null, 2));
578
612
  }
579
613
  const INTERACTIVE_LABEL_WIDTH = 24;
614
+ const INTERACTIVE_DISCOVERY_TIMEOUT_MS = 800;
615
+ const INTERACTIVE_BINDING_TIMEOUT_MS = 800;
616
+ const INTERACTIVE_ACCOUNT_LIMITS_TIMEOUT_MS = 700;
617
+ const SESSION_STATE_CHECK_TIMEOUT_MS = 3000;
618
+ const STATUS_BINDING_TIMEOUT_MS = 800;
580
619
  function canRenderInteractiveShell() {
581
620
  return Boolean(node_process_1.stdin.isTTY && node_process_1.stdout.isTTY);
582
621
  }
@@ -628,11 +667,8 @@ function shellListLine(value) {
628
667
  return `• ${value}`;
629
668
  }
630
669
  function buildInteractiveOverviewSections(service, sessionState, accountLimits) {
631
- const overview = service.getConnectStatusOverview();
632
- const inbox = service.buildSmartRegistrationInbox();
633
- const leftoverReady = inbox.groups.ready_to_connect.slice(0, 4).map((item) => shellListLine(item.display_name));
634
- const leftoverNeedsConfirmation = inbox.groups.needs_confirmation.slice(0, 4).map((item) => shellListLine(item.display_name));
635
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
670
+ const accountTrusted = sessionState === 'authorized';
671
+ const overview = service.getConnectStatusOverview({ includeInbox: false });
636
672
  const preparedWorkspace = accountTrusted ? String(overview.workspace_id || '').trim() : '';
637
673
  const preparedProject = accountTrusted ? String(overview.project_id || '').trim() : '';
638
674
  // Base section: always visible
@@ -673,6 +709,9 @@ function buildInteractiveOverviewSections(service, sessionState, accountLimits)
673
709
  lines: [
674
710
  shellLine('Plan', accountLimits.planName),
675
711
  shellLine('Private passports', formatRemainingLimit(accountLimits.privatePassportsLimit, accountLimits.privatePassportsUsed, 'private passport')),
712
+ ...(accountLimits.governedPassportsLimit !== null || accountLimits.governedPassportsUsed !== null
713
+ ? [shellLine('Governed passports', formatRemainingLimit(accountLimits.governedPassportsLimit, accountLimits.governedPassportsUsed, 'governed passport'))]
714
+ : []),
676
715
  shellLine('Drafts', formatRemainingLimit(accountLimits.draftLimit, accountLimits.draftsUsed, 'draft')),
677
716
  shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
678
717
  shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
@@ -687,10 +726,8 @@ function buildInteractiveOverviewSections(service, sessionState, accountLimits)
687
726
  lines: [
688
727
  shellLine('Workspace', formatScopeReferenceLabel(preparedWorkspace || null, 'workspace')),
689
728
  shellLine('Project', formatScopeReferenceLabel(preparedProject || null, 'project')),
690
- shellLine('Ready to connect', inbox.groups.ready_to_connect.length),
691
- shellLine('Needs confirmation', inbox.groups.needs_confirmation.length),
692
- ...leftoverReady,
693
- ...leftoverNeedsConfirmation,
729
+ shellLine('Ready to connect', overview.ready_to_connect_count),
730
+ shellLine('Needs confirmation', overview.needs_confirmation_count),
694
731
  ],
695
732
  });
696
733
  }
@@ -1227,7 +1264,9 @@ async function checkBackendSessionState(service) {
1227
1264
  baseUrl: DEFAULT_BASE_URL,
1228
1265
  sessionRef: sessionRefValue,
1229
1266
  });
1230
- const result = await api.getProfileAccess();
1267
+ const result = await withTimeout(api.getProfileAccess(), SESSION_STATE_CHECK_TIMEOUT_MS, null);
1268
+ if (!result)
1269
+ return 'unavailable';
1231
1270
  if (result.ok)
1232
1271
  return 'authorized';
1233
1272
  if (result.status === 401 || result.status === 403)
@@ -1434,18 +1473,27 @@ async function run() {
1434
1473
  const runPublicConnectInit = () => {
1435
1474
  printConnectInit(service.initializeConnectIdentity());
1436
1475
  };
1476
+ const withSmartInboxSnapshotCounts = (overview) => {
1477
+ const inbox = service.getSmartRegistrationInbox({
1478
+ preferSnapshot: true,
1479
+ refreshInBackground: false,
1480
+ });
1481
+ return {
1482
+ ...overview,
1483
+ ready_to_connect_count: inbox.summary.ready_to_connect_count,
1484
+ needs_confirmation_count: inbox.summary.needs_confirmation_count,
1485
+ connected_count: inbox.summary.connected_count,
1486
+ next_recommended_action: inbox.summary.next_recommended_action,
1487
+ };
1488
+ };
1437
1489
  const runPublicConnectStatus = async () => {
1438
1490
  const sessionState = await checkBackendSessionState(service);
1491
+ const secureStorage = service.getCredentialStoreStatus();
1439
1492
  if (service.readSessionRef()) {
1440
- try {
1441
- await service.refreshEffectiveBinding();
1442
- }
1443
- catch {
1444
- // Status remains useful with the last local binding snapshot.
1445
- }
1493
+ await withTimeout(service.refreshEffectiveBinding(), STATUS_BINDING_TIMEOUT_MS, undefined);
1446
1494
  }
1447
- const overview = service.getConnectStatusOverview();
1448
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1495
+ const accountTrusted = sessionState === 'authorized';
1496
+ const overview = withSmartInboxSnapshotCounts(service.getConnectStatusOverview({ includeInbox: false }));
1449
1497
  const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1450
1498
  const displayOverview = accountTrusted
1451
1499
  ? overview
@@ -1466,23 +1514,50 @@ async function run() {
1466
1514
  session_truth: accountTrusted ? 'account_verified_or_offline' : 'local_scope_cached_login_required',
1467
1515
  local_scope_cached: !accountTrusted && localScopeCached,
1468
1516
  binding_truth: accountTrusted ? 'account_binding_active' : 'account_login_required',
1517
+ secure_storage: {
1518
+ backend: secureStorage.backend,
1519
+ available: secureStorage.available,
1520
+ plaintext_fallback_active: secureStorage.plaintextFallbackActive,
1521
+ legacy_plaintext_file_present: secureStorage.legacyPlaintextFilePresent,
1522
+ detail: secureStorage.detail,
1523
+ },
1469
1524
  });
1470
1525
  return;
1471
1526
  }
1472
1527
  printPublicStatusOverview(displayOverview);
1473
1528
  console.log(`- session=${sessionState}`);
1474
1529
  console.log(`- binding_truth=${accountTrusted ? 'account_binding_active' : 'account_login_required'}`);
1530
+ console.log(`- secure_storage_backend=${secureStorage.backend} available=${secureStorage.available} plaintext_fallback=${secureStorage.plaintextFallbackActive}`);
1531
+ if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
1532
+ console.log(`- secure_storage_detail=${secureStorage.detail}`);
1533
+ }
1475
1534
  if (!accountTrusted && localScopeCached) {
1476
1535
  console.log('- local_scope_cached=true');
1477
1536
  }
1478
1537
  printPublicStatusGuidance(displayOverview, sessionState);
1479
1538
  };
1480
- const runPublicConnectInbox = () => {
1481
- const inbox = service.buildSmartRegistrationInbox();
1539
+ const runPublicConnectInbox = async () => {
1540
+ const forceRefresh = hasFlag('--refresh');
1541
+ const inbox = service.getSmartRegistrationInbox({
1542
+ forceRefresh,
1543
+ preferSnapshot: !forceRefresh,
1544
+ refreshInBackground: !forceRefresh,
1545
+ });
1482
1546
  if (hasFlag('--json')) {
1483
1547
  printJson(inbox);
1484
1548
  return;
1485
1549
  }
1550
+ const freshness = inbox.summary.freshness_state ?? 'fresh';
1551
+ const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
1552
+ ? Number(inbox.summary.snapshot_age_seconds)
1553
+ : 0;
1554
+ console.log(`[forkit-connect] Inbox snapshot freshness=${freshness} age_seconds=${ageSeconds}`);
1555
+ if (freshness === 'stale') {
1556
+ console.log('[forkit-connect] Snapshot is stale. Run `forkit-connect inbox --refresh` for immediate reconcile.');
1557
+ }
1558
+ else if (freshness === 'syncing') {
1559
+ console.log('[forkit-connect] Background reconcile is running; results will self-refresh after completion.');
1560
+ }
1486
1561
  printSmartInbox(inbox);
1487
1562
  };
1488
1563
  const runPublicCollectedChanges = () => {
@@ -1571,10 +1646,11 @@ async function run() {
1571
1646
  const runPublicLogout = () => {
1572
1647
  const currentSessionRef = String(service.readSessionRef() || '').trim();
1573
1648
  const hadEnvironmentSession = Boolean(String(process.env.FORKIT_CONNECT_SESSION_REF || '').trim());
1649
+ const logoutAt = new Date().toISOString();
1574
1650
  delete process.env.FORKIT_CONNECT_SESSION_REF;
1575
1651
  try {
1576
1652
  service.setSessionRef(null);
1577
- console.log('[forkit-connect] Logged out. Local discovery remains available on this device.');
1653
+ console.log(`[forkit-connect] Logged out at ${logoutAt}. Local discovery remains available on this device.`);
1578
1654
  if (hadEnvironmentSession) {
1579
1655
  console.log('[forkit-connect] The in-process fallback session was cleared for this run.');
1580
1656
  }
@@ -1583,12 +1659,12 @@ async function run() {
1583
1659
  catch (error) {
1584
1660
  if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1585
1661
  if (currentSessionRef) {
1586
- console.log('[forkit-connect] Logged out from the current interactive run.');
1662
+ console.log(`[forkit-connect] Logged out from the current interactive run at ${logoutAt}.`);
1587
1663
  console.log('[forkit-connect] If you previously exported a session in your shell, remove it there with:');
1588
1664
  console.log('unset FORKIT_CONNECT_SESSION_REF');
1589
1665
  return;
1590
1666
  }
1591
- console.log('[forkit-connect] No stored session found.');
1667
+ console.log(`[forkit-connect] No stored session found at ${logoutAt}.`);
1592
1668
  console.log('[forkit-connect] If you previously exported a session in this shell, remove it with:');
1593
1669
  console.log('unset FORKIT_CONNECT_SESSION_REF');
1594
1670
  return;
@@ -1644,6 +1720,7 @@ async function run() {
1644
1720
  try {
1645
1721
  service.setSessionRef(polled.body.connect_access_token);
1646
1722
  await service.refreshEffectiveBinding();
1723
+ await withTimeout(service.prewarmSmartRegistrationInbox(), 1800, undefined);
1647
1724
  const displayName = getSessionDisplayName(polled.body.connect_access_token);
1648
1725
  console.log(displayName
1649
1726
  ? `[forkit-connect] Login approved. Welcome, ${displayName}. Session credentials stored securely.`
@@ -1653,6 +1730,7 @@ async function run() {
1653
1730
  catch (error) {
1654
1731
  if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1655
1732
  await activateEnvironmentSessionFallback(polled.body.connect_access_token);
1733
+ await withTimeout(service.prewarmSmartRegistrationInbox(), 1800, undefined);
1656
1734
  printSessionExportFallback(polled.body.connect_access_token);
1657
1735
  const displayName = getSessionDisplayName(polled.body.connect_access_token);
1658
1736
  console.log(displayName
@@ -1709,16 +1787,11 @@ async function run() {
1709
1787
  const buildWorkspaceStatusPayload = async () => {
1710
1788
  const sessionState = await checkBackendSessionState(service);
1711
1789
  if (service.readSessionRef()) {
1712
- try {
1713
- await service.refreshEffectiveBinding();
1714
- }
1715
- catch {
1716
- // Keep local scope view available.
1717
- }
1790
+ await withTimeout(service.refreshEffectiveBinding(), STATUS_BINDING_TIMEOUT_MS, undefined);
1718
1791
  }
1719
- const overview = service.getConnectStatusOverview();
1720
1792
  const operatingMode = resolveOperatingMode(service);
1721
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1793
+ const accountTrusted = sessionState === 'authorized';
1794
+ const overview = withSmartInboxSnapshotCounts(service.getConnectStatusOverview({ includeInbox: false }));
1722
1795
  const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1723
1796
  return {
1724
1797
  session_state: sessionState,
@@ -1739,7 +1812,9 @@ async function run() {
1739
1812
  };
1740
1813
  };
1741
1814
  const renderInteractiveStatusScreen = async (sessionState) => {
1742
- const accountLimits = await loadCliAccountLimits().catch(() => null);
1815
+ const accountLimits = node_process_1.stdin.isTTY && node_process_1.stdout.isTTY
1816
+ ? await withTimeout(loadCliAccountLimits().catch(() => null), INTERACTIVE_ACCOUNT_LIMITS_TIMEOUT_MS, null)
1817
+ : null;
1743
1818
  const displayName = accountLimits?.displayName ?? getSessionDisplayName(service.readSessionRef());
1744
1819
  renderInteractiveScreen('Forkit Connect', {
1745
1820
  subtitle: displayName ? `Welcome back, ${displayName}` : 'Interactive overview',
@@ -1788,18 +1863,8 @@ async function run() {
1788
1863
  // Only run discovery and binding refresh when the user has a session.
1789
1864
  // Pre-login, these calls add significant startup latency with no user benefit.
1790
1865
  if (service.readSessionRef()) {
1791
- try {
1792
- await service.runDiscoveryCycle();
1793
- }
1794
- catch {
1795
- // Keep the interactive shell usable even if local discovery has transient issues.
1796
- }
1797
- try {
1798
- await service.refreshEffectiveBinding();
1799
- }
1800
- catch {
1801
- // Use the latest local binding snapshot when backend refresh is unavailable.
1802
- }
1866
+ await withTimeout(service.runDiscoveryCycle(), INTERACTIVE_DISCOVERY_TIMEOUT_MS, undefined);
1867
+ await withTimeout(service.refreshEffectiveBinding(), INTERACTIVE_BINDING_TIMEOUT_MS, undefined);
1803
1868
  }
1804
1869
  };
1805
1870
  const runInteractiveWorkspaceMenu = async () => {
@@ -2394,7 +2459,10 @@ async function run() {
2394
2459
  }
2395
2460
  };
2396
2461
  const buildInteractiveRegisterCandidates = () => {
2397
- const inbox = service.buildSmartRegistrationInbox();
2462
+ const inbox = service.getSmartRegistrationInbox({
2463
+ preferSnapshot: true,
2464
+ refreshInBackground: true,
2465
+ });
2398
2466
  const candidates = new Map();
2399
2467
  const groups = ['needs_confirmation', 'ready_to_connect'];
2400
2468
  const state = service.getStateStore().readState();
@@ -2654,7 +2722,10 @@ async function run() {
2654
2722
  await runInteractiveAutoRefresh();
2655
2723
  const groupOrder = ['needs_confirmation', 'ready_to_connect', 'connected', 'ignored'];
2656
2724
  while (true) {
2657
- const inbox = service.buildSmartRegistrationInbox();
2725
+ const inbox = service.getSmartRegistrationInbox({
2726
+ preferSnapshot: true,
2727
+ refreshInBackground: true,
2728
+ });
2658
2729
  const entries = groupOrder.flatMap((group) => inbox.groups[group].map((item) => ({ group, item })));
2659
2730
  if (entries.length === 0) {
2660
2731
  renderInteractiveScreen('Smart Registration Inbox', {
@@ -2669,7 +2740,7 @@ async function run() {
2669
2740
  return;
2670
2741
  }
2671
2742
  renderInteractiveScreen('Smart Registration Inbox', {
2672
- subtitle: `Generated at ${inbox.summary.generated_at}`,
2743
+ subtitle: `Generated at ${inbox.summary.generated_at} · freshness=${inbox.summary.freshness_state ?? 'fresh'}`,
2673
2744
  sections: buildInteractiveInboxSections(inbox),
2674
2745
  footerLines: ['Choose an inbox item below.'],
2675
2746
  });
@@ -2704,11 +2775,15 @@ async function run() {
2704
2775
  }
2705
2776
  };
2706
2777
  const runInteractiveStart = async () => {
2778
+ if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
2779
+ const sessionState = service.readSessionRef() ? 'unavailable' : 'missing';
2780
+ await renderInteractiveStatusScreen(sessionState);
2781
+ return;
2782
+ }
2707
2783
  while (true) {
2708
2784
  process.exitCode = 0;
2709
- await runInteractiveAutoRefresh();
2710
2785
  const sessionState = await checkBackendSessionState(service);
2711
- const authenticated = sessionState === 'authorized' || (sessionState === 'unavailable' && Boolean(service.readSessionRef()));
2786
+ const authenticated = sessionState === 'authorized';
2712
2787
  if (!authenticated) {
2713
2788
  await renderInteractiveStatusScreen(sessionState);
2714
2789
  const selected = await promptSelection('Choose an action', [
@@ -2765,6 +2840,8 @@ async function run() {
2765
2840
  { value: 'exit', label: 'Exit' },
2766
2841
  ]);
2767
2842
  if (!selected || selected === 'exit') {
2843
+ const exitAt = new Date().toISOString();
2844
+ console.log(`[forkit-connect] Quit at ${exitAt}. Interactive work stopped for this device; use logout if you want to clear the session reference.`);
2768
2845
  return;
2769
2846
  }
2770
2847
  if (selected === 'status') {
@@ -2964,6 +3041,10 @@ async function run() {
2964
3041
  runtimeSignalsUsed: null,
2965
3042
  runtimeSignalsLimit: fallbackPlan.runtimeSignalsPerMonth,
2966
3043
  runtimeSignalsRemaining: null,
3044
+ governedPassportsUsed: null,
3045
+ governedPassportsLimit: fallbackPlan.maxGovernedPassports,
3046
+ governedPassportsRemaining: null,
3047
+ publishedModelPassports: [],
2967
3048
  };
2968
3049
  cachedCliAccountLimits = fallback;
2969
3050
  cachedCliAccountLimitsAt = now;
@@ -3027,6 +3108,21 @@ async function run() {
3027
3108
  const runtimeSignalsUsed = typeof summaryPayload?.usage?.runtimeSignals === 'number'
3028
3109
  ? summaryPayload.usage.runtimeSignals
3029
3110
  : null;
3111
+ const governedPassportsUsed = typeof summaryPayload?.usage?.passports === 'number'
3112
+ ? summaryPayload.usage.passports
3113
+ : passports.length;
3114
+ const governedPassportsLimit = summaryPayload?.planCapabilities?.governance?.maxGovernedPassports
3115
+ ?? summaryPayload?.entitlements?.maxGovernedPassports
3116
+ ?? fallback.maxGovernedPassports;
3117
+ const publishedModelPassports = passports
3118
+ .filter((passport) => String(passport.passportType || passport.passport_type || passport.type || '').toLowerCase() === 'model')
3119
+ .map((passport) => ({
3120
+ gaid: String(passport.gaid || '').trim(),
3121
+ name: String(passport.name || 'Unnamed model passport').trim(),
3122
+ workspaceId: String(passport.workspaceId || passport.workspace_id || '').trim() || null,
3123
+ projectId: String(passport.projectId || passport.project_id || '').trim() || null,
3124
+ }))
3125
+ .filter((passport) => passport.gaid);
3030
3126
  const resolved = {
3031
3127
  displayName,
3032
3128
  planKey,
@@ -3047,6 +3143,10 @@ async function run() {
3047
3143
  runtimeSignalsUsed,
3048
3144
  runtimeSignalsLimit,
3049
3145
  runtimeSignalsRemaining: remainingFromLimit(runtimeSignalsLimit, runtimeSignalsUsed),
3146
+ governedPassportsUsed,
3147
+ governedPassportsLimit,
3148
+ governedPassportsRemaining: remainingFromLimit(governedPassportsLimit, governedPassportsUsed),
3149
+ publishedModelPassports,
3050
3150
  };
3051
3151
  cachedCliAccountLimits = resolved;
3052
3152
  cachedCliAccountLimitsAt = now;
@@ -3298,41 +3398,91 @@ async function run() {
3298
3398
  return 'Draft creation is not active for this binding yet. Complete Connect approval or update consent on Forkit.dev first.';
3299
3399
  return raw;
3300
3400
  };
3301
- const runRegisterOne = async (targetModelName) => {
3401
+ const normalizeRegisterSuccessMessage = (action) => {
3402
+ if (action === 'already_bound')
3403
+ return 'Model is already connected to an existing passport.';
3404
+ if (action === 'already_pending')
3405
+ return 'Model already has a pending draft. No duplicate draft created.';
3406
+ if (action === 'passport_registered')
3407
+ return 'Passport published successfully.';
3408
+ if (action === 'draft_created')
3409
+ return 'Draft created successfully.';
3410
+ if (action === 'draft_queued')
3411
+ return 'Draft queued locally and will sync when backend access is available.';
3412
+ return action;
3413
+ };
3414
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
3415
+ const governedPassportCapacityFull = accountLimits
3416
+ ? isCapacityExhausted(accountLimits.governedPassportsLimit, accountLimits.governedPassportsRemaining)
3417
+ : false;
3418
+ const buildCapacityPayload = (requestedModel) => ({
3419
+ ok: false,
3420
+ code: 'GOVERNED_PASSPORT_CAPACITY_REACHED',
3421
+ requested_model: requestedModel ?? null,
3422
+ plan: accountLimits?.planName ?? operatingMode.tier ?? null,
3423
+ governed_passports_used: accountLimits?.governedPassportsUsed ?? null,
3424
+ governed_passports_limit: accountLimits?.governedPassportsLimit ?? null,
3425
+ message: 'This account has reached governed passport capacity. Forkit Connect will not create more governed drafts by default because publishing would be blocked.',
3426
+ next_actions: [
3427
+ 'Use an existing published model passport for Runtime Signals C2 or run-log testing.',
3428
+ 'Free capacity or upgrade before publishing another governed model.',
3429
+ 'Use --draft-only if you intentionally want to save another draft for later review.',
3430
+ ],
3431
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
3432
+ });
3433
+ const runRegisterOne = async (targetModelSelector, displayNameHint) => {
3302
3434
  try {
3303
- const result = await service.connectDetectedModel(targetModelName);
3435
+ const result = await service.connectDetectedModel(targetModelSelector);
3304
3436
  return {
3305
3437
  ok: true,
3306
3438
  model: result.model.model,
3439
+ selector: result.model.discoveryHash,
3307
3440
  draftId: result.draftId ?? null,
3308
3441
  gaid: result.gaid ?? null,
3309
- message: result.action,
3442
+ message: normalizeRegisterSuccessMessage(result.action),
3443
+ action: result.action,
3310
3444
  };
3311
3445
  }
3312
3446
  catch (error) {
3313
3447
  const rawMessage = error instanceof Error ? error.message : 'register_failed';
3314
3448
  return {
3315
3449
  ok: false,
3316
- model: targetModelName,
3450
+ model: displayNameHint || targetModelSelector,
3451
+ selector: targetModelSelector,
3317
3452
  message: normalizeRegisterErrorMessage(rawMessage),
3318
3453
  };
3319
3454
  }
3320
3455
  };
3321
3456
  if (hasFlag('--all-ready')) {
3457
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
3458
+ printJson({
3459
+ ...buildCapacityPayload(null),
3460
+ attempted: 0,
3461
+ skipped: 'all-ready',
3462
+ });
3463
+ process.exitCode = 2;
3464
+ return;
3465
+ }
3322
3466
  const inbox = service.buildSmartRegistrationInbox();
3323
- const readyModels = inbox.groups.ready_to_connect
3467
+ const readyModelCandidates = inbox.groups.ready_to_connect
3324
3468
  .filter((item) => item.item_type === 'model' && item.recommended_action === 'create_passport_draft')
3325
- .map((item) => item.display_name);
3326
- if (readyModels.length === 0) {
3469
+ .map((item) => ({
3470
+ selector: extractInboxItemSelector(item),
3471
+ model: item.display_name,
3472
+ }))
3473
+ .filter((item) => String(item.selector || '').trim())
3474
+ .sort((left, right) => left.model.localeCompare(right.model) || left.selector.localeCompare(right.selector));
3475
+ if (readyModelCandidates.length === 0) {
3327
3476
  console.log('No ready local models need registration.');
3328
3477
  return;
3329
3478
  }
3330
3479
  const results = [];
3331
- for (const item of readyModels) {
3332
- results.push(await runRegisterOne(item));
3480
+ for (const item of readyModelCandidates) {
3481
+ results.push(await runRegisterOne(item.selector, item.model));
3333
3482
  }
3334
3483
  printJson({
3335
- attempted: readyModels.length,
3484
+ attempted: readyModelCandidates.length,
3485
+ selectors: readyModelCandidates.map((item) => item.selector),
3336
3486
  results,
3337
3487
  });
3338
3488
  if (results.some((item) => !item.ok)) {
@@ -3345,18 +3495,41 @@ async function run() {
3345
3495
  const readyModels = inbox.groups.ready_to_connect
3346
3496
  .filter((item) => item.item_type === 'model')
3347
3497
  .map((item) => item.display_name);
3498
+ const readyModelSelectors = inbox.groups.ready_to_connect
3499
+ .filter((item) => item.item_type === 'model')
3500
+ .map((item) => ({
3501
+ model: item.display_name,
3502
+ selector: extractInboxItemSelector(item),
3503
+ }))
3504
+ .filter((item) => String(item.selector || '').trim())
3505
+ .sort((left, right) => left.model.localeCompare(right.model) || left.selector.localeCompare(right.selector));
3348
3506
  printJson({
3349
3507
  operating_mode: operatingMode.mode,
3350
3508
  tier: operatingMode.tier,
3351
3509
  workspace_id: boundWorkspaceId,
3352
3510
  project_id: boundProjectId,
3511
+ capacity: accountLimits ? {
3512
+ governed_passports_used: accountLimits.governedPassportsUsed,
3513
+ governed_passports_limit: accountLimits.governedPassportsLimit,
3514
+ governed_passports_remaining: accountLimits.governedPassportsRemaining,
3515
+ capacity_full: governedPassportCapacityFull,
3516
+ } : null,
3353
3517
  ready_models: readyModels,
3518
+ ready_model_selectors: readyModelSelectors,
3519
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
3354
3520
  next: readyModels.length
3355
- ? 'Run forkit-connect register --model "<name>" or forkit-connect register --all-ready'
3521
+ ? governedPassportCapacityFull
3522
+ ? 'Capacity is full. Use an existing passport for C2/run-log testing, free capacity, upgrade, or pass --draft-only if you intentionally want another draft.'
3523
+ : 'Run forkit-connect register --model "<name>" or forkit-connect register --all-ready'
3356
3524
  : 'Run forkit-connect scan first or review forkit-connect inbox',
3357
3525
  });
3358
3526
  return;
3359
3527
  }
3528
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
3529
+ printJson(buildCapacityPayload(modelName));
3530
+ process.exitCode = 2;
3531
+ return;
3532
+ }
3360
3533
  const result = await runRegisterOne(modelName);
3361
3534
  printJson(result);
3362
3535
  if (!result.ok) {
@@ -3408,7 +3581,7 @@ async function run() {
3408
3581
  return;
3409
3582
  }
3410
3583
  if (command === 'inbox') {
3411
- runPublicConnectInbox();
3584
+ await runPublicConnectInbox();
3412
3585
  return;
3413
3586
  }
3414
3587
  if (command === 'start') {
@@ -3505,7 +3678,7 @@ async function run() {
3505
3678
  return;
3506
3679
  }
3507
3680
  if (subcommand === 'inbox') {
3508
- runPublicConnectInbox();
3681
+ await runPublicConnectInbox();
3509
3682
  return;
3510
3683
  }
3511
3684
  if (subcommand === 'services') {
@@ -3650,6 +3823,54 @@ async function run() {
3650
3823
  }, null, 2));
3651
3824
  return;
3652
3825
  }
3826
+ if (subcommand === 'run-log' && args[2] === 'emit') {
3827
+ const gaid = getArg('--gaid') ?? getArg('--heartbeat-gaid');
3828
+ const apiKey = getArg('--api-key') ?? getArg('--heartbeat-key') ?? process.env.FORKIT_RUNTIME_SIGNAL_API_KEY ?? null;
3829
+ const provider = getArg('--provider');
3830
+ const runModel = getArg('--model');
3831
+ const serviceName = getArg('--service-name') ?? getArg('--name');
3832
+ if (!gaid || !provider || !runModel || !serviceName) {
3833
+ console.error('[forkit-connect] c2 run-log emit requires --gaid, --provider, --model, and --service-name.');
3834
+ console.error('[forkit-connect] Provide an API key with --api-key, --heartbeat-key, FORKIT_RUNTIME_SIGNAL_API_KEY, or a stored c2 set-key entry.');
3835
+ process.exitCode = 1;
3836
+ return;
3837
+ }
3838
+ const result = await service.emitRuntimeRunLog({
3839
+ gaid,
3840
+ apiKey,
3841
+ provider,
3842
+ model: runModel,
3843
+ serviceName,
3844
+ serviceKind: getArg('--service-kind') ?? 'custom',
3845
+ runId: getArg('--run-id'),
3846
+ externalRunId: getArg('--external-run-id'),
3847
+ status: getArg('--status') ?? 'completed',
3848
+ startedAt: getArg('--started-at'),
3849
+ endedAt: getArg('--ended-at'),
3850
+ promptTokens: getNumericArg('--prompt-tokens'),
3851
+ completionTokens: getNumericArg('--completion-tokens'),
3852
+ cachedPromptTokens: getNumericArg('--cached-prompt-tokens'),
3853
+ reasoningTokens: getNumericArg('--reasoning-tokens'),
3854
+ latencyMs: getNumericArg('--latency-ms'),
3855
+ estimatedCostCents: getNumericArg('--estimated-cost-cents'),
3856
+ currency: getArg('--currency') ?? 'USD',
3857
+ summary: getArg('--summary'),
3858
+ });
3859
+ if (!result.ok) {
3860
+ console.error(`[forkit-connect] Runtime run log emit failed (${result.status || 'local'}).`);
3861
+ if (result.body) {
3862
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
3863
+ }
3864
+ process.exitCode = 2;
3865
+ return;
3866
+ }
3867
+ console.log(JSON.stringify({
3868
+ accepted: true,
3869
+ status: result.status,
3870
+ response: result.body,
3871
+ }, null, 2));
3872
+ return;
3873
+ }
3653
3874
  if (subcommand === 'sync') {
3654
3875
  const syncResult = await service.flushC2LifecycleEvents({
3655
3876
  runtimeSignalApiKey: getArg('--heartbeat-key'),
@@ -4441,6 +4662,29 @@ async function run() {
4441
4662
  process.exitCode = 2;
4442
4663
  return;
4443
4664
  }
4665
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
4666
+ const governedPassportCapacityFull = accountLimits
4667
+ ? isCapacityExhausted(accountLimits.governedPassportsLimit, accountLimits.governedPassportsRemaining)
4668
+ : false;
4669
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
4670
+ printJson({
4671
+ ok: false,
4672
+ code: 'GOVERNED_PASSPORT_CAPACITY_REACHED',
4673
+ requested_model: modelName,
4674
+ plan: accountLimits?.planName ?? null,
4675
+ governed_passports_used: accountLimits?.governedPassportsUsed ?? null,
4676
+ governed_passports_limit: accountLimits?.governedPassportsLimit ?? null,
4677
+ message: 'This account has reached governed passport capacity. Forkit Connect will not create another governed draft by default because publishing would be blocked.',
4678
+ next_actions: [
4679
+ 'Use an existing published model passport for Runtime Signals C2 or run-log testing.',
4680
+ 'Free capacity or upgrade before publishing another governed model.',
4681
+ 'Use --draft-only if you intentionally want to save another draft for later review.',
4682
+ ],
4683
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
4684
+ });
4685
+ process.exitCode = 2;
4686
+ return;
4687
+ }
4444
4688
  const modelKey = `${model.model}#${model.digest}`;
4445
4689
  const existingBinding = (state.model_bindings || []).find((binding) => binding.modelKey === modelKey && binding.status !== 'ignored');
4446
4690
  if (existingBinding?.draftId) {