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/README.md +32 -4
- package/dist/cli.js +310 -66
- package/dist/v1/api.d.ts +62 -1
- package/dist/v1/api.js +131 -2
- package/dist/v1/credential-store.d.ts +1 -0
- package/dist/v1/credential-store.js +15 -3
- package/dist/v1/service.d.ts +55 -3
- package/dist/v1/service.js +605 -49
- package/dist/v1/state.d.ts +1 -0
- package/dist/v1/state.js +32 -12
- package/dist/v1/types.d.ts +55 -1
- package/package.json +3 -3
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]
|
|
226
|
-
console.log(
|
|
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
|
|
632
|
-
const
|
|
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',
|
|
691
|
-
shellLine('Needs confirmation',
|
|
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
|
-
|
|
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
|
|
1448
|
-
const
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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'
|
|
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 =
|
|
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
|
-
|
|
1792
|
-
|
|
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.
|
|
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.
|
|
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'
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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) =>
|
|
3326
|
-
|
|
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
|
|
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:
|
|
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
|
-
?
|
|
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) {
|