forkit-connect 0.1.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.
Files changed (51) hide show
  1. package/QUICKSTART.md +55 -0
  2. package/README.md +96 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +4724 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.js +21 -0
  7. package/dist/launcher.d.ts +33 -0
  8. package/dist/launcher.js +9344 -0
  9. package/dist/ps-list-loader.d.ts +5 -0
  10. package/dist/ps-list-loader.js +20 -0
  11. package/dist/v1/agent-observation.d.ts +42 -0
  12. package/dist/v1/agent-observation.js +499 -0
  13. package/dist/v1/api.d.ts +276 -0
  14. package/dist/v1/api.js +390 -0
  15. package/dist/v1/credential-store.d.ts +92 -0
  16. package/dist/v1/credential-store.js +797 -0
  17. package/dist/v1/currency.d.ts +41 -0
  18. package/dist/v1/currency.js +127 -0
  19. package/dist/v1/daemon.d.ts +50 -0
  20. package/dist/v1/daemon.js +265 -0
  21. package/dist/v1/discovery.d.ts +61 -0
  22. package/dist/v1/discovery.js +168 -0
  23. package/dist/v1/filesystem-models.d.ts +11 -0
  24. package/dist/v1/filesystem-models.js +261 -0
  25. package/dist/v1/heartbeat.d.ts +45 -0
  26. package/dist/v1/heartbeat.js +463 -0
  27. package/dist/v1/lifecycle-monitor.d.ts +78 -0
  28. package/dist/v1/lifecycle-monitor.js +512 -0
  29. package/dist/v1/lmstudio.d.ts +11 -0
  30. package/dist/v1/lmstudio.js +148 -0
  31. package/dist/v1/ollama.d.ts +19 -0
  32. package/dist/v1/ollama.js +164 -0
  33. package/dist/v1/openai-compatible.d.ts +12 -0
  34. package/dist/v1/openai-compatible.js +124 -0
  35. package/dist/v1/process-scout.d.ts +50 -0
  36. package/dist/v1/process-scout.js +715 -0
  37. package/dist/v1/providers.d.ts +50 -0
  38. package/dist/v1/providers.js +106 -0
  39. package/dist/v1/service.d.ts +680 -0
  40. package/dist/v1/service.js +8286 -0
  41. package/dist/v1/state.d.ts +87 -0
  42. package/dist/v1/state.js +1318 -0
  43. package/dist/v1/test-credential-backend.d.ts +19 -0
  44. package/dist/v1/test-credential-backend.js +49 -0
  45. package/dist/v1/types.d.ts +873 -0
  46. package/dist/v1/types.js +3 -0
  47. package/dist/v1/update.d.ts +38 -0
  48. package/dist/v1/update.js +184 -0
  49. package/dist/v1/vitality-pulse.d.ts +36 -0
  50. package/dist/v1/vitality-pulse.js +512 -0
  51. package/package.json +53 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4724 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_readline_1 = require("node:readline");
6
+ const promises_1 = require("node:readline/promises");
7
+ const node_process_1 = require("node:process");
8
+ const daemon_1 = require("./v1/daemon");
9
+ const service_1 = require("./v1/service");
10
+ const discovery_1 = require("./v1/discovery");
11
+ const heartbeat_1 = require("./v1/heartbeat");
12
+ const update_1 = require("./v1/update");
13
+ const api_1 = require("./v1/api");
14
+ const credential_store_1 = require("./v1/credential-store");
15
+ const DEFAULT_BASE_URL = process.env.FORKIT_API_URL ?? process.env.FORKIT_BASE_URL ?? 'https://www.forkit.dev';
16
+ const TRAIN_EVENT_TYPES = [
17
+ 'training_started',
18
+ 'fine_tuning_started',
19
+ 'checkpoint_created',
20
+ 'evaluation_logged',
21
+ 'training_completed',
22
+ 'artifact_ready',
23
+ 'version_created',
24
+ 'draft_published',
25
+ 'build_aborted',
26
+ ];
27
+ const TRAIN_DATASET_CHANGE_TYPES = [
28
+ 'initial_dataset',
29
+ 'fine_tuning_dataset',
30
+ 'validation_dataset',
31
+ 'test_dataset',
32
+ 'dataset_removed',
33
+ 'dataset_updated',
34
+ ];
35
+ const CLI_FALLBACK_PLAN_LIMITS = {
36
+ origin: {
37
+ privatePassports: 0,
38
+ draftPassports: 0,
39
+ maxWorkspaces: 0,
40
+ maxProjects: 0,
41
+ runtimeSignalsPerMonth: null,
42
+ },
43
+ signal: {
44
+ privatePassports: 25,
45
+ draftPassports: 25,
46
+ maxWorkspaces: 3,
47
+ maxProjects: 3,
48
+ runtimeSignalsPerMonth: 10000,
49
+ },
50
+ protocol: {
51
+ privatePassports: 20,
52
+ draftPassports: 20,
53
+ maxWorkspaces: null,
54
+ maxProjects: null,
55
+ runtimeSignalsPerMonth: 250000,
56
+ },
57
+ sovereign: {
58
+ privatePassports: null,
59
+ draftPassports: null,
60
+ maxWorkspaces: null,
61
+ maxProjects: null,
62
+ runtimeSignalsPerMonth: null,
63
+ },
64
+ };
65
+ const PUBLIC_COMMANDS = [
66
+ ['init', 'Initialize local Connect identity and privacy posture'],
67
+ ['login', 'Link this device to your Forkit.dev account'],
68
+ ['logout', 'Remove the stored Forkit.dev session from this device'],
69
+ ['status', 'Show account, scope, daemon, and queue status'],
70
+ ['changes', 'View all collected local changes and queued sync items'],
71
+ ['start', 'Open the interactive launcher or start foreground sync'],
72
+ ['stop', 'Stop the local Connect daemon'],
73
+ ['scan', 'Discover runtimes, models, and agents on this device'],
74
+ ['inbox', 'Review the Smart Registration Inbox'],
75
+ ['sync', 'Flush queued drafts and lifecycle metadata'],
76
+ ['workspace', 'List, select, or inspect optional governed workspace/project scope'],
77
+ ['register', 'Register ready local models into the current scope'],
78
+ ['ignore', 'Ignore one detected local model'],
79
+ ['doctor', 'Run local environment diagnostics'],
80
+ ];
81
+ const ADVANCED_COMMAND_GROUPS = [
82
+ ['connect', 'Model connection, runtime review, and handoff utilities'],
83
+ ['review', 'Summarize discovered but unconnected items'],
84
+ ['doctor', 'Run local environment diagnostics'],
85
+ ['workspaces', 'List accessible workspaces after sign-in'],
86
+ ['projects', 'List projects for a workspace'],
87
+ ['bind', 'Bind the local device to a workspace and project'],
88
+ ['register', 'Create a Passport draft for a detected model'],
89
+ ['drafts', 'List backend Passport drafts'],
90
+ ['publish', 'Publish a Passport draft'],
91
+ ['bound', 'Inspect bound Passport models'],
92
+ ['heartbeat', 'Send a runtime heartbeat for a bound Passport'],
93
+ ['update-check', 'Check release metadata without auto-updating'],
94
+ ['config', 'Inspect or set local Connect config'],
95
+ ['daemon', 'Advanced daemon controls'],
96
+ ['pulse', 'Inspect best-effort local runtime signals'],
97
+ ['c2', 'Metadata-only lifecycle sync utilities'],
98
+ ['train', 'Training lifecycle evidence commands'],
99
+ ['agent', 'Agent review, link, and runtime signal utilities'],
100
+ ['tray', 'Tray/status surface helpers'],
101
+ ['notify', 'Notification preview and delivery controls'],
102
+ ];
103
+ function usage() {
104
+ console.log('Usage: forkit-connect <init|login|logout|status|changes|start|stop|scan|inbox|sync|workspace|register|ignore|doctor> [options]');
105
+ console.log(' forkit-connect workspace <list|select|create|status> [options]');
106
+ console.log('Public commands:');
107
+ for (const [command, description] of PUBLIC_COMMANDS) {
108
+ console.log(` ${command.padEnd(7)} ${description}`);
109
+ }
110
+ console.log('Options:');
111
+ console.log(' --description <text> Optional workspace description used by workspace create');
112
+ console.log(' --project-name <text> Project name used by workspace create/select');
113
+ console.log(' --project-description <text> Optional project description used by workspace create/select');
114
+ console.log(' --json Print machine-readable output when supported');
115
+ console.log(' --model <value> Model name used by register');
116
+ console.log(' --all-ready Register every ready local model in the current scope');
117
+ console.log(' --ignore-model <name> Ignore detected model by name with --ignore-digest');
118
+ console.log(' --ignore-digest <sha> Digest used with --ignore-model');
119
+ console.log(' --digest <sha> Digest used by ignore when the model name is ambiguous');
120
+ console.log(' --interval-seconds <n> Override daemon scan interval');
121
+ console.log(' --no-browser Do not auto-open verification URL during login');
122
+ console.log(' --advanced-help Show internal/engineering commands');
123
+ }
124
+ function advancedUsage() {
125
+ usage();
126
+ console.log('');
127
+ console.log('Advanced commands:');
128
+ console.log(' forkit-connect connect <modelNameOrDiscoveryHash>');
129
+ console.log(' forkit-connect connect <start|init|status|inbox|services|permissions|handoff|evolution review|runtime review|runtime status>');
130
+ console.log(' forkit-connect <review|workspaces|projects|bind|drafts|publish|bound|heartbeat|update-check|config|daemon|pulse|c2|train|agent|tray|notify> [options]');
131
+ for (const [command, description] of ADVANCED_COMMAND_GROUPS) {
132
+ console.log(` ${command.padEnd(12)} ${description}`);
133
+ }
134
+ console.log('Advanced options:');
135
+ console.log(' --session-ref <value> Store/update backend session reference');
136
+ console.log(' --workspace <id> Store workspace binding');
137
+ console.log(' --project <id> Store project binding');
138
+ console.log(' --draft <id> Draft id used by publish');
139
+ console.log(' --name <value> Model or metric name depending on command');
140
+ console.log(' --framework <value> Training framework used by train init');
141
+ console.log(' --task <value> Training task used by train init');
142
+ console.log(' --dataset-ref <value> Safe dataset reference used by train init');
143
+ console.log(' --type <value> Training lifecycle event type');
144
+ console.log(' --ref <value> Dataset reference used by train dataset');
145
+ console.log(' --change <value> Dataset change type used by train dataset');
146
+ console.log(' --value <number> Metric value used by train metric');
147
+ console.log(' --version <value> Version name used by train version');
148
+ console.log(' --reason <value> Version or lifecycle reason');
149
+ console.log(' --path <value> Artifact path used by train artifact');
150
+ console.log(' --hash-artifact Hash artifact contents for train artifact');
151
+ console.log(' --heartbeat-gaid <gaid> Queue heartbeat runtime signal event for GAID');
152
+ console.log(' --heartbeat-key <key> API key used for heartbeat runtime signal event');
153
+ console.log(' Also used by: c2 set-key (stores key + backfills events)');
154
+ }
155
+ function showUsage() {
156
+ if (hasFlag('--advanced-help')) {
157
+ advancedUsage();
158
+ return;
159
+ }
160
+ usage();
161
+ }
162
+ function getArg(flag) {
163
+ const args = process.argv.slice(2);
164
+ const index = args.indexOf(flag);
165
+ if (index < 0)
166
+ return null;
167
+ return args[index + 1] ?? null;
168
+ }
169
+ function hasFlag(flag) {
170
+ return process.argv.slice(2).includes(flag);
171
+ }
172
+ function isHelpCommand(command) {
173
+ return command === 'help' || command === '--help' || command === '-h' || command === '--advanced-help';
174
+ }
175
+ function sleep(ms) {
176
+ return new Promise((resolve) => {
177
+ const delay = globalThis['set' + 'Timeout'];
178
+ if (typeof delay !== 'function') {
179
+ resolve();
180
+ return;
181
+ }
182
+ delay(resolve, ms);
183
+ });
184
+ }
185
+ function isDeviceConnectStartResponse(body) {
186
+ if (!body || typeof body !== 'object')
187
+ return false;
188
+ const value = body;
189
+ return (typeof value.device_code === 'string' &&
190
+ typeof value.user_code === 'string' &&
191
+ typeof value.verification_url === 'string' &&
192
+ typeof value.expires_at === 'string' &&
193
+ typeof value.poll_interval_seconds === 'number');
194
+ }
195
+ function isPollPending(body) {
196
+ return !!body && typeof body === 'object' && body.status === 'pending';
197
+ }
198
+ function isPollApproved(body) {
199
+ if (!body || typeof body !== 'object')
200
+ return false;
201
+ const value = body;
202
+ return value.status === 'approved' && typeof value.connect_access_token === 'string';
203
+ }
204
+ function openUrl(url) {
205
+ return new Promise((resolve) => {
206
+ if (process.platform === 'win32') {
207
+ (0, node_child_process_1.exec)(`cmd /c start "" ${JSON.stringify(url)}`, (error) => resolve(!error));
208
+ return;
209
+ }
210
+ const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
211
+ (0, node_child_process_1.exec)(`${opener} ${JSON.stringify(url)}`, (error) => resolve(!error));
212
+ });
213
+ }
214
+ function printDeviceLoginInstructions(start) {
215
+ console.log('[forkit-connect] Browser approval required.');
216
+ console.log(`[forkit-connect] User code: ${start.user_code}`);
217
+ console.log('[forkit-connect] Open this URL in your browser:');
218
+ console.log(start.verification_url);
219
+ console.log(`[forkit-connect] Expires at: ${start.expires_at}`);
220
+ console.log('[forkit-connect] Keep this terminal open while Forkit Connect waits for approval.');
221
+ }
222
+ function printSessionExportFallback(token) {
223
+ console.log('[forkit-connect] Approval succeeded, but secure credential storage is unavailable in this Linux session.');
224
+ 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.');
229
+ }
230
+ function hasLinuxGuiSession() {
231
+ if (process.platform !== 'linux') {
232
+ return true;
233
+ }
234
+ return Boolean(String(process.env.DISPLAY || '').trim()
235
+ || String(process.env.WAYLAND_DISPLAY || '').trim()
236
+ || String(process.env.MIR_SOCKET || '').trim());
237
+ }
238
+ function parseJwtPayload(token) {
239
+ const raw = String(token || '').trim();
240
+ const parts = raw.split('.');
241
+ if (parts.length !== 3) {
242
+ return null;
243
+ }
244
+ try {
245
+ const json = Buffer.from(parts[1] || '', 'base64url').toString('utf8');
246
+ const payload = JSON.parse(json);
247
+ return payload && typeof payload === 'object' ? payload : null;
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ }
253
+ function isProfileAccessResponse(body) {
254
+ return !!body && typeof body === 'object';
255
+ }
256
+ function isProductSummaryResponse(body) {
257
+ return !!body && typeof body === 'object';
258
+ }
259
+ function resolveCliPlanKey(...values) {
260
+ for (const value of values) {
261
+ const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '');
262
+ if (!normalized)
263
+ continue;
264
+ if (['origin', 'free', 'starter', 'launch'].includes(normalized))
265
+ return 'origin';
266
+ if (['signal', 'solo', 'singlepro', 'pro'].includes(normalized))
267
+ return 'signal';
268
+ if (['protocol', 'team', 'business'].includes(normalized))
269
+ return 'protocol';
270
+ if (['sovereign', 'enterprise'].includes(normalized))
271
+ return 'sovereign';
272
+ }
273
+ return 'origin';
274
+ }
275
+ function getCliPlanName(planKey) {
276
+ switch (planKey) {
277
+ case 'signal':
278
+ return 'Signal';
279
+ case 'protocol':
280
+ return 'Protocol';
281
+ case 'sovereign':
282
+ return 'Sovereign';
283
+ default:
284
+ return 'Origin';
285
+ }
286
+ }
287
+ function remainingFromLimit(limit, used) {
288
+ if (!Number.isFinite(limit))
289
+ return null;
290
+ const safeLimit = Number(limit);
291
+ const safeUsed = Number.isFinite(used) ? Number(used) : 0;
292
+ return Math.max(safeLimit - safeUsed, 0);
293
+ }
294
+ function formatRemainingLimit(limit, used, singular, plural = `${singular}s`) {
295
+ if (!Number.isFinite(limit)) {
296
+ return `Unlimited ${plural}`;
297
+ }
298
+ const safeLimit = Number(limit);
299
+ const safeUsed = Number.isFinite(used) ? Number(used) : 0;
300
+ const remaining = Math.max(safeLimit - safeUsed, 0);
301
+ return `${remaining} left (${safeUsed}/${safeLimit} used)`;
302
+ }
303
+ function formatWorkspaceAccessLine(workspace) {
304
+ const workspaceId = String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown');
305
+ return `- ${summarizeWorkspaceLabel(workspace)} | id=${workspaceId}`;
306
+ }
307
+ function isWorkspaceProjectsResponse(body) {
308
+ return !!body && typeof body === 'object';
309
+ }
310
+ function isWorkspaceCreateResponse(body) {
311
+ return !!body && typeof body === 'object';
312
+ }
313
+ function isWorkspaceProjectCreateResponse(body) {
314
+ return !!body && typeof body === 'object';
315
+ }
316
+ function formatWorkspaceProjectLine(project) {
317
+ const projectId = String(project.id || 'unknown');
318
+ return `- ${summarizeProjectLabel(project)} | id=${projectId}`;
319
+ }
320
+ function resolveOperatingMode(service) {
321
+ const entitlements = service.getServiceEntitlements();
322
+ const normalizedTier = String(entitlements.tier || '').trim().toLowerCase();
323
+ if (['origin', 'free', 'solo'].includes(normalizedTier)) {
324
+ return {
325
+ mode: 'solo',
326
+ tier: normalizedTier || null,
327
+ };
328
+ }
329
+ if (normalizedTier) {
330
+ return {
331
+ mode: 'governed',
332
+ tier: normalizedTier,
333
+ };
334
+ }
335
+ const permissions = service.getConnectPermissions();
336
+ const draftRule = permissions.rules.find((rule) => rule.action === 'connect_model_draft');
337
+ const reason = String(draftRule?.reason || '').toLowerCase();
338
+ const scopeRequired = reason.includes('workspace/project') || reason.includes('scope');
339
+ return {
340
+ mode: scopeRequired ? 'governed' : 'solo',
341
+ tier: null,
342
+ };
343
+ }
344
+ function getSessionDisplayName(sessionRef) {
345
+ const payload = parseJwtPayload(sessionRef);
346
+ const candidates = [
347
+ payload?.name,
348
+ payload?.display_name,
349
+ payload?.given_name,
350
+ payload?.preferred_username,
351
+ payload?.email,
352
+ ];
353
+ for (const candidate of candidates) {
354
+ if (typeof candidate === 'string' && candidate.trim()) {
355
+ return candidate.trim();
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ function isPassportDraftListResponse(body) {
361
+ return !!body && typeof body === 'object';
362
+ }
363
+ function isPassportDraftResponse(body) {
364
+ return !!body && typeof body === 'object';
365
+ }
366
+ function isPassportDraftPublishResponse(body) {
367
+ return !!body && typeof body === 'object';
368
+ }
369
+ function isPassportResponse(body) {
370
+ return !!body && typeof body === 'object';
371
+ }
372
+ function readDraftMetadata(draft) {
373
+ const payload = draft.payload && typeof draft.payload === 'object' ? draft.payload : null;
374
+ const metadata = payload?.metadata;
375
+ return metadata && typeof metadata === 'object' && !Array.isArray(metadata)
376
+ ? metadata
377
+ : {};
378
+ }
379
+ function formatDraftLine(draft) {
380
+ const metadata = readDraftMetadata(draft);
381
+ const workspaceId = String(metadata.workspaceId || metadata.workspace_id || 'n/a');
382
+ const projectId = String(metadata.projectId || metadata.project_id || 'n/a');
383
+ const payload = draft.payload && typeof draft.payload === 'object' ? draft.payload : null;
384
+ const connectionStatus = String(payload?.connectionStatus || metadata.connection_status || 'n/a');
385
+ const draftId = String(draft.id || 'unknown');
386
+ const name = String(draft.name || 'Unnamed draft');
387
+ const passportType = String(draft.passportType || 'unknown');
388
+ return `- ${draftId} | ${name} | type=${passportType} | workspace=${workspaceId} | project=${projectId} | connection=${connectionStatus}`;
389
+ }
390
+ function deriveModelSizeLabel(sizeBytes) {
391
+ const size = Number(sizeBytes);
392
+ if (!Number.isFinite(size) || size <= 0) {
393
+ return 'size unknown';
394
+ }
395
+ const gib = size / (1024 ** 3);
396
+ if (gib >= 1) {
397
+ return `${gib.toFixed(gib >= 10 ? 0 : 1)} GB download`;
398
+ }
399
+ const mib = size / (1024 ** 2);
400
+ return `${mib.toFixed(mib >= 100 ? 0 : 1)} MB download`;
401
+ }
402
+ function readModelFamily(model) {
403
+ const metadata = model.metadata && typeof model.metadata === 'object' ? model.metadata : null;
404
+ const family = String(metadata?.family || metadata?.architectureFamily || model.architectureFamily || '').trim();
405
+ return family || null;
406
+ }
407
+ function readModelParameterLabel(model) {
408
+ const metadata = model.metadata && typeof model.metadata === 'object' ? model.metadata : null;
409
+ const parameterSize = String(model.parameterSize || metadata?.parameterSize || metadata?.parameter_count || '').trim();
410
+ if (!parameterSize)
411
+ return null;
412
+ return /b$/i.test(parameterSize) ? `${parameterSize.toUpperCase()} parameters` : parameterSize;
413
+ }
414
+ function formatModelFriendlySummary(model) {
415
+ const family = readModelFamily(model);
416
+ const parameters = readModelParameterLabel(model);
417
+ const runtime = String(model.runtimeName || model.runtime || 'local runtime').trim();
418
+ const highlights = [family, parameters, deriveModelSizeLabel(model.sizeBytes)].filter(Boolean);
419
+ return `${model.model} runs on ${runtime}${highlights.length ? ` · ${highlights.join(' · ')}` : ''}`;
420
+ }
421
+ function formatModelFriendlyExplanation(model) {
422
+ const family = readModelFamily(model);
423
+ const source = String(model.sourceLabel || 'Detected locally').trim();
424
+ if (family) {
425
+ return `${model.model} looks like a ${family} model detected on this device. ${source}.`;
426
+ }
427
+ return `${model.model} is a local model detected on this device. ${source}.`;
428
+ }
429
+ function readPassportList(body) {
430
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
431
+ return [];
432
+ }
433
+ const passports = body.passports;
434
+ if (!Array.isArray(passports)) {
435
+ return [];
436
+ }
437
+ return passports.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item));
438
+ }
439
+ function formatPassportLine(passport) {
440
+ const gaid = String(passport.gaid || passport.id || 'unknown');
441
+ const name = String(passport.name || 'Unnamed passport');
442
+ const passportType = String(passport.passportType || 'unknown');
443
+ const workspaceId = String(passport.workspaceId || 'n/a');
444
+ const projectId = String(passport.projectId || 'n/a');
445
+ const connectionStatus = String(passport.connectionStatus || 'n/a');
446
+ return `- ${gaid} | ${name} | type=${passportType} | workspace=${workspaceId} | project=${projectId} | connection=${connectionStatus}`;
447
+ }
448
+ function formatWorkspaceVisibility(value) {
449
+ const visibility = String(value || '').trim().toLowerCase();
450
+ if (visibility === 'private' || visibility === 'public') {
451
+ return visibility;
452
+ }
453
+ return 'visibility not set';
454
+ }
455
+ function summarizeWorkspaceLabel(workspace) {
456
+ const name = String(workspace.name || 'Unnamed workspace').trim();
457
+ const role = String(workspace.role || 'unknown').trim();
458
+ const visibility = formatWorkspaceVisibility(workspace.visibility);
459
+ const description = String(workspace.description || '').trim();
460
+ const detail = [visibility, role].filter(Boolean).join(' · ');
461
+ return `${name}${detail ? ` · ${detail}` : ''}${description ? ` — ${description}` : ''}`;
462
+ }
463
+ function summarizeProjectLabel(project) {
464
+ const name = String(project.name || 'Unnamed project').trim();
465
+ const status = String(project.status || '').trim();
466
+ const passportCount = Number(project.passportCount);
467
+ const description = String(project.description || '').trim();
468
+ const detailParts = [
469
+ status || null,
470
+ Number.isFinite(passportCount) && passportCount >= 0 ? `${passportCount} passport${passportCount === 1 ? '' : 's'}` : null,
471
+ ].filter((value) => Boolean(value));
472
+ return `${name}${detailParts.length ? ` · ${detailParts.join(' · ')}` : ''}${description ? ` — ${description}` : ''}`;
473
+ }
474
+ function buildPassportWebsiteUrl(gaid) {
475
+ return `${DEFAULT_BASE_URL}/passport/${encodeURIComponent(gaid)}`;
476
+ }
477
+ function buildDraftWebsiteUrl(draftId) {
478
+ return `${DEFAULT_BASE_URL}/register-passport?draftId=${encodeURIComponent(draftId)}`;
479
+ }
480
+ function summarizeScopeChoice(workspacePrepared) {
481
+ if (workspacePrepared) {
482
+ return 'Workspace mode lets you pick another workspace/project or keep using the current one.';
483
+ }
484
+ return 'Workspace mode lets you choose an existing workspace/project or create new ones now.';
485
+ }
486
+ function formatScopeReferenceLabel(value, kind) {
487
+ const normalized = String(value || '').trim();
488
+ if (!normalized) {
489
+ return kind === 'workspace' ? 'not selected' : 'not selected';
490
+ }
491
+ return `selected on this device (${shortId(normalized)})`;
492
+ }
493
+ function printUpdateCheckLines(lines, useErrorStream = false) {
494
+ for (const line of lines) {
495
+ if (useErrorStream) {
496
+ console.error(line);
497
+ continue;
498
+ }
499
+ console.log(line);
500
+ }
501
+ }
502
+ function formatFindingLine(finding) {
503
+ const label = finding.classification === 'shadow_candidate'
504
+ ? 'Unconnected AI runtime'
505
+ : finding.classification === 'provider_unavailable'
506
+ ? 'Runtime unavailable'
507
+ : finding.classification === 'registered_runtime'
508
+ ? 'Connected local runtime'
509
+ : 'Unconnected AI model';
510
+ const nextStep = finding.classification === 'provider_unavailable'
511
+ ? 'Check whether this runtime should be installed and running.'
512
+ : finding.classification === 'shadow_candidate'
513
+ ? 'Review this runtime before connecting it.'
514
+ : 'Review or connect this local model.';
515
+ if (finding.kind === 'process') {
516
+ const process = finding.process;
517
+ const confidence = process?.confidence ? ` | confidence=${process.confidence}` : '';
518
+ const source = process?.source_label ? ` | source=${process.source_label}` : '';
519
+ const matchedTerms = process?.matched_terms?.length ? ` | matched=${process.matched_terms.join(',')}` : '';
520
+ return `- ${label} | process=${finding.process_name || 'unknown'} | runtime=${finding.runtime || 'unknown'}${confidence}${source}${matchedTerms} | next=${nextStep}`;
521
+ }
522
+ if (finding.kind === 'provider') {
523
+ return `- ${label} | runtime=${finding.runtime || 'unknown'} | next=${nextStep}`;
524
+ }
525
+ return `- ${label} | model=${finding.model || 'unknown'} | runtime=${finding.runtime || 'unknown'} | next=${nextStep}`;
526
+ }
527
+ function formatReviewStatusLabel(status) {
528
+ switch (status) {
529
+ case 'new_unregistered':
530
+ case 'known_unregistered':
531
+ return 'Unconnected AI models';
532
+ case 'pending_draft':
533
+ return 'Pending Passport drafts';
534
+ case 'bound_passport':
535
+ return 'Connected models';
536
+ case 'shadow_candidate':
537
+ return 'Unconnected AI runtimes';
538
+ case 'provider_unavailable':
539
+ return 'Unavailable runtimes';
540
+ }
541
+ }
542
+ function formatReviewActionLabel(action) {
543
+ switch (action) {
544
+ case 'connect_model':
545
+ return 'Run `forkit-connect connect <model>` when you are ready.';
546
+ case 'review_drafts':
547
+ return 'Review the local Passport draft.';
548
+ case 'already_connected':
549
+ return 'Already connected.';
550
+ case 'review/runtime':
551
+ return 'Check whether this runtime should be installed and running.';
552
+ case 'review/connect':
553
+ case 'review_model':
554
+ return 'Review or connect this local AI item.';
555
+ default:
556
+ return action;
557
+ }
558
+ }
559
+ function printDiscoverySummary(summary) {
560
+ console.log(`[forkit-connect] Discovery mode=${summary.discovery_mode} providers=${summary.providers_detected} models=${summary.models_detected} new_unregistered=${summary.new_unregistered_count} known_unregistered=${summary.known_unregistered_count} registered_runtime=${summary.registered_runtime_count} shadow_candidate=${summary.shadow_candidate_count} provider_unavailable=${summary.provider_unavailable_count}`);
561
+ for (const finding of summary.findings) {
562
+ console.log(formatFindingLine(finding));
563
+ }
564
+ }
565
+ function printConfig(config) {
566
+ console.log(JSON.stringify(config, null, 2));
567
+ }
568
+ function printConnectInit(result) {
569
+ console.log(JSON.stringify({
570
+ identity: result.identity,
571
+ entitlements: result.entitlements,
572
+ permissions: result.permissions,
573
+ privacy_summary: result.privacy_summary,
574
+ }, null, 2));
575
+ }
576
+ function printJson(value) {
577
+ console.log(JSON.stringify(value, null, 2));
578
+ }
579
+ const INTERACTIVE_LABEL_WIDTH = 24;
580
+ function canRenderInteractiveShell() {
581
+ return Boolean(node_process_1.stdin.isTTY && node_process_1.stdout.isTTY);
582
+ }
583
+ function clearInteractiveScreen() {
584
+ if (!canRenderInteractiveShell())
585
+ return;
586
+ node_process_1.stdout.write('\u001B[2J\u001B[H');
587
+ }
588
+ function renderInteractiveScreen(title, options) {
589
+ if (!canRenderInteractiveShell()) {
590
+ console.log(`[forkit-connect] ${title}`);
591
+ if (options?.subtitle) {
592
+ console.log(options.subtitle);
593
+ }
594
+ for (const section of options?.sections || []) {
595
+ console.log(`${section.title}:`);
596
+ for (const line of section.lines) {
597
+ console.log(line);
598
+ }
599
+ }
600
+ for (const line of options?.footerLines || []) {
601
+ console.log(line);
602
+ }
603
+ return;
604
+ }
605
+ clearInteractiveScreen();
606
+ const bold = '\u001B[1m';
607
+ const dim = '\u001B[2m';
608
+ const reset = '\u001B[0m';
609
+ const divider = `${dim}${'─'.repeat(72)}${reset}`;
610
+ const lines = [
611
+ `${bold}${title}${reset}`,
612
+ options?.subtitle ? `${dim}${options.subtitle}${reset}` : null,
613
+ divider,
614
+ ...((options?.sections || []).flatMap((section) => [
615
+ `${bold}${section.title}${reset}`,
616
+ ...(section.lines.length > 0 ? section.lines : [`${dim}No items yet.${reset}`]),
617
+ '',
618
+ ])),
619
+ ...(options?.footerLines?.length ? [divider, ...options.footerLines] : []),
620
+ ].filter((value) => typeof value === 'string');
621
+ node_process_1.stdout.write(`${lines.join('\n')}\n`);
622
+ }
623
+ function shellLine(label, value) {
624
+ const normalized = value === null || value === undefined || `${value}`.trim() === '' ? 'n/a' : String(value);
625
+ return `• ${`${label}:`.padEnd(INTERACTIVE_LABEL_WIDTH, ' ')}${normalized}`;
626
+ }
627
+ function shellListLine(value) {
628
+ return `• ${value}`;
629
+ }
630
+ 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';
636
+ const preparedWorkspace = accountTrusted ? String(overview.workspace_id || '').trim() : '';
637
+ const preparedProject = accountTrusted ? String(overview.project_id || '').trim() : '';
638
+ // Base section: always visible
639
+ const sections = [
640
+ {
641
+ title: 'Overview',
642
+ lines: [
643
+ shellLine('Session', sessionState),
644
+ shellLine('Device paired', overview.device_paired),
645
+ shellLine('Daemon', overview.daemon_status),
646
+ shellLine('Privacy mode', overview.privacy_mode),
647
+ ],
648
+ },
649
+ ];
650
+ // Account-bound sections: only shown after login
651
+ if (accountTrusted) {
652
+ sections.push({
653
+ title: 'Discovery',
654
+ lines: [
655
+ shellLine('Models', overview.models_discovered),
656
+ shellLine('Agents', overview.agents_discovered),
657
+ shellLine('Runtimes', overview.runtimes_discovered),
658
+ shellLine('C2 sync pending', overview.c2_sync_pending),
659
+ ],
660
+ });
661
+ sections.push({
662
+ title: 'Inbox',
663
+ lines: [
664
+ shellLine('Ready to connect', overview.ready_to_connect_count),
665
+ shellLine('Needs confirmation', overview.needs_confirmation_count),
666
+ shellLine('Connected', overview.connected_count),
667
+ ...(overview.lifecycle_note ? [shellLine('Note', overview.lifecycle_note)] : []),
668
+ ],
669
+ });
670
+ if (accountLimits) {
671
+ sections.push({
672
+ title: 'Plan & Limits',
673
+ lines: [
674
+ shellLine('Plan', accountLimits.planName),
675
+ shellLine('Private passports', formatRemainingLimit(accountLimits.privatePassportsLimit, accountLimits.privatePassportsUsed, 'private passport')),
676
+ shellLine('Drafts', formatRemainingLimit(accountLimits.draftLimit, accountLimits.draftsUsed, 'draft')),
677
+ shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
678
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
679
+ ...(accountLimits.runtimeSignalsLimit !== null || accountLimits.runtimeSignalsUsed !== null
680
+ ? [shellLine('Runtime signals', formatRemainingLimit(accountLimits.runtimeSignalsLimit, accountLimits.runtimeSignalsUsed, 'runtime signal'))]
681
+ : []),
682
+ ],
683
+ });
684
+ }
685
+ sections.push({
686
+ title: 'Active Scope',
687
+ lines: [
688
+ shellLine('Workspace', formatScopeReferenceLabel(preparedWorkspace || null, 'workspace')),
689
+ 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,
694
+ ],
695
+ });
696
+ }
697
+ return sections;
698
+ }
699
+ function buildInteractiveChangesSections(service, limit) {
700
+ const state = service.getStateStore().readState();
701
+ const recentEvidence = [...state.evidence_events].slice(-limit).reverse();
702
+ const recentPulse = service.getPulseHistory(limit);
703
+ const evidenceTypeCounts = [...state.evidence_events.reduce((map, event) => {
704
+ map.set(event.type, (map.get(event.type) || 0) + 1);
705
+ return map;
706
+ }, new Map()).entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
707
+ return [
708
+ {
709
+ title: 'Collected State',
710
+ lines: [
711
+ shellLine('Evidence events', state.evidence_events.length),
712
+ shellLine('Pulse events', state.pulse_events.length),
713
+ shellLine('Sync queue', state.sync_queue.length),
714
+ shellLine('Pending reviews', state.pending_reviews.length),
715
+ shellLine('Evolution candidates', state.evolution_candidates.length),
716
+ ],
717
+ },
718
+ {
719
+ title: 'Evidence Types',
720
+ lines: evidenceTypeCounts.slice(0, 8).map(([type, count]) => shellLine(type, count)),
721
+ },
722
+ {
723
+ title: 'Recent Evidence',
724
+ lines: recentEvidence.slice(0, 8).map((event) => shellListLine(`${formatTimestamp(event.createdAt)} | ${event.type}${formatEvidenceDetails(event.details)}`)),
725
+ },
726
+ {
727
+ title: 'Recent Runtime Signals',
728
+ lines: recentPulse.slice(0, 6).map((event) => shellListLine(`${event.runtime_name} | ${event.model_name || 'n/a'} | ${event.pulse_status} | ${event.measured_at}`)),
729
+ },
730
+ ];
731
+ }
732
+ function buildInteractiveInboxSections(inbox) {
733
+ const summarize = (items, limit = 4) => (items.slice(0, limit).map((item) => shellListLine(`${item.display_name} | ${item.item_type} | ${formatSmartInboxActionLabel(item)}`)));
734
+ return [
735
+ {
736
+ title: 'Summary',
737
+ lines: [
738
+ shellLine('Ready to connect', inbox.groups.ready_to_connect.length),
739
+ shellLine('Needs confirmation', inbox.groups.needs_confirmation.length),
740
+ shellLine('Connected', inbox.groups.connected.length),
741
+ shellLine('Ignored', inbox.groups.ignored.length),
742
+ ...(inbox.summary.next_recommended_action ? [shellLine('Next action', formatSmartInboxActionValue(inbox.summary.next_recommended_action))] : []),
743
+ ],
744
+ },
745
+ {
746
+ title: 'Needs Confirmation',
747
+ lines: summarize(inbox.groups.needs_confirmation),
748
+ },
749
+ {
750
+ title: 'Ready to Connect',
751
+ lines: summarize(inbox.groups.ready_to_connect),
752
+ },
753
+ ];
754
+ }
755
+ function buildInteractiveInboxItemSections(item, group) {
756
+ const formattedGroup = group === 'ready_to_connect'
757
+ ? 'Ready'
758
+ : group === 'needs_confirmation'
759
+ ? 'Needs confirmation'
760
+ : group === 'connected'
761
+ ? 'Connected'
762
+ : group === 'ignored'
763
+ ? 'Ignored'
764
+ : group.replaceAll('_', ' ');
765
+ const detailKeys = [
766
+ 'verification_summary',
767
+ 'limitations_summary',
768
+ 'scope_suggestion',
769
+ 'scope_mismatch_reason',
770
+ 'connectable_model_name',
771
+ 'linked_model_candidate',
772
+ 'public_state',
773
+ 'last_seen',
774
+ ];
775
+ return [
776
+ {
777
+ title: 'Item',
778
+ lines: [
779
+ shellLine('Name', item.display_name),
780
+ shellLine('Group', formattedGroup),
781
+ shellLine('Type', item.item_type),
782
+ shellLine('Source', item.detected_source),
783
+ shellLine('Confidence', item.confidence),
784
+ shellLine('Next', formattedGroup === 'Connected' ? 'View activity logs' : formatSmartInboxActionLabel(item)),
785
+ ...(item.workspaceId ? [shellLine('Workspace', formatScopeReferenceLabel(item.workspaceId, 'workspace'))] : []),
786
+ ...(item.projectId ? [shellLine('Project', formatScopeReferenceLabel(item.projectId, 'project'))] : []),
787
+ ...(item.passport_gaid ? [shellLine('Passport GAID', item.passport_gaid)] : []),
788
+ ...(item.matched_passport_gaid && item.matched_passport_gaid !== item.passport_gaid ? [shellLine('Matched passport', item.matched_passport_gaid)] : []),
789
+ ],
790
+ },
791
+ {
792
+ title: 'Details',
793
+ lines: detailKeys
794
+ .map((key) => {
795
+ const value = String(item.details_received_automatically[key] || '').trim();
796
+ return value ? shellLine(key, value) : null;
797
+ })
798
+ .filter((value) => typeof value === 'string'),
799
+ },
800
+ ];
801
+ }
802
+ function normalizeComparable(value) {
803
+ return String(value || '').trim().toLowerCase();
804
+ }
805
+ function getInboxItemModelName(item) {
806
+ if (item.item_type === 'model') {
807
+ return String(item.display_name || '').trim() || null;
808
+ }
809
+ const connectable = String(item.details_received_automatically.connectable_model_name || '').trim();
810
+ return connectable || null;
811
+ }
812
+ function matchesEvidenceToConnectedItem(event, options) {
813
+ const details = event.details || {};
814
+ const values = [
815
+ details.modelName,
816
+ details.model_name,
817
+ details.model,
818
+ details.discoveryHash,
819
+ details.discovery_hash,
820
+ details.registrationKey,
821
+ details.registration_key,
822
+ details.passportGaid,
823
+ details.passport_gaid,
824
+ details.gaid,
825
+ ].map(normalizeComparable);
826
+ return Boolean((options.modelName && values.includes(normalizeComparable(options.modelName)))
827
+ || (options.discoveryHash && values.includes(normalizeComparable(options.discoveryHash)))
828
+ || (options.registrationKey && values.includes(normalizeComparable(options.registrationKey)))
829
+ || (options.passportGaid && values.includes(normalizeComparable(options.passportGaid))));
830
+ }
831
+ function matchesPulseToConnectedItem(event, options) {
832
+ return Boolean((options.modelName && normalizeComparable(event.model_name) === normalizeComparable(options.modelName))
833
+ || (options.discoveryHash && normalizeComparable(event.discovery_hash) === normalizeComparable(options.discoveryHash))
834
+ || (options.registrationKey && normalizeComparable(event.registration_key) === normalizeComparable(options.registrationKey))
835
+ || (options.passportGaid && normalizeComparable(event.bound_passport_gaid) === normalizeComparable(options.passportGaid)));
836
+ }
837
+ function buildConnectedItemActivitySections(service, item) {
838
+ const state = service.getStateStore().readState();
839
+ const selector = item.item_id.split(':').slice(1).join(':');
840
+ const detectedModel = item.item_type === 'model'
841
+ ? state.detected_models.find((entry) => entry.discoveryHash === selector)
842
+ ?? resolveSingleDetectedModelByName(service, item.display_name)
843
+ : null;
844
+ const discoveryHash = detectedModel?.discoveryHash || selector || null;
845
+ const binding = state.model_bindings.find((entry) => ((item.passport_gaid && entry.gaid === item.passport_gaid)
846
+ || (discoveryHash && entry.discoveryHash === discoveryHash))) ?? null;
847
+ const modelName = getInboxItemModelName(item);
848
+ const recentEvidence = state.evidence_events
849
+ .filter((event) => matchesEvidenceToConnectedItem(event, {
850
+ modelName,
851
+ discoveryHash,
852
+ registrationKey: binding?.registrationKey ?? null,
853
+ passportGaid: binding?.gaid ?? item.passport_gaid ?? null,
854
+ }))
855
+ .slice(-6)
856
+ .reverse();
857
+ const recentPulse = service.getPulseHistory(50)
858
+ .filter((event) => matchesPulseToConnectedItem(event, {
859
+ modelName,
860
+ discoveryHash,
861
+ registrationKey: binding?.registrationKey ?? null,
862
+ passportGaid: binding?.gaid ?? item.passport_gaid ?? null,
863
+ }))
864
+ .slice(-6)
865
+ .reverse();
866
+ return [
867
+ {
868
+ title: 'Connection',
869
+ lines: [
870
+ shellLine('Model', modelName || item.display_name),
871
+ shellLine('Connected', binding?.status === 'bound' || Boolean(item.passport_gaid) ? 'yes' : 'no'),
872
+ shellLine('Passport', binding?.gaid || item.passport_gaid || 'not linked yet'),
873
+ shellLine('Workspace', binding?.workspaceName || formatScopeReferenceLabel(binding?.workspaceId ?? null, 'workspace')),
874
+ shellLine('Project', binding?.projectName || formatScopeReferenceLabel(binding?.projectId ?? null, 'project')),
875
+ shellLine('Last updated', binding?.updatedAt || 'n/a'),
876
+ ],
877
+ },
878
+ {
879
+ title: 'Recent Activity',
880
+ lines: recentEvidence.length > 0
881
+ ? recentEvidence.map((event) => shellListLine(`${formatTimestamp(event.createdAt)} · ${event.type}`))
882
+ : [shellListLine('No local activity logs recorded for this connected item yet.')],
883
+ },
884
+ {
885
+ title: 'Runtime Signals',
886
+ lines: recentPulse.length > 0
887
+ ? recentPulse.map((event) => shellListLine(`${event.measured_at} · ${event.runtime_name} · ${event.pulse_status}`))
888
+ : [shellListLine('No runtime signal history recorded for this connected item yet.')],
889
+ },
890
+ ];
891
+ }
892
+ function formatPulseMetric(value, suffix) {
893
+ return value === null || value === undefined ? 'n/a' : `${value}${suffix}`;
894
+ }
895
+ function pulseMetricNote(metric, fallback) {
896
+ return typeof metric?.note === 'string' && metric.note.trim()
897
+ ? metric.note
898
+ : fallback;
899
+ }
900
+ function formatPulseMetricWithNote(value, suffix, unavailableNote) {
901
+ return value === null || value === undefined ? unavailableNote : `${value}${suffix}`;
902
+ }
903
+ function printPulseSummary(summary) {
904
+ console.log(`[forkit-connect] Runtime signals pulse measured_at=${summary.measured_at} overall=${summary.pulse_status} observation_mode=${summary.observation_mode || 'metadata_only'} degraded_mode=${summary.degraded_mode || 'normal'} green=${summary.green_count} amber=${summary.amber_count} red=${summary.red_count} unknown=${summary.unknown_count}`);
905
+ console.log('[forkit-connect] Runtime signals fields are best-effort local measurements and are not billing records.');
906
+ if (summary.degraded_reasons?.length) {
907
+ console.log(`[forkit-connect] Degraded observation: ${summary.degraded_reasons.join(' | ')}`);
908
+ }
909
+ for (const entry of summary.entries) {
910
+ console.log(`- runtime=${entry.runtime_name} | model=${entry.model_name || 'n/a'} | registration=${entry.registration_state} | observation_mode=${entry.observation_mode || 'metadata_only'} | degraded_mode=${entry.degraded_mode || 'normal'} | port_alive=${String(entry.port_alive)} | process_alive=${String(entry.process_alive)} | provider_status=${entry.provider_status} | pulse_status=${entry.pulse_status}`);
911
+ console.log(` metrics: cpu=${formatPulseMetricWithNote(entry.cpu_usage_percent, '%', pulseMetricNote(entry.metric_availability?.cpu_usage_percent, 'CPU metrics were unavailable this cycle.'))} | memory=${formatPulseMetricWithNote(entry.memory_usage_mb, 'MB', pulseMetricNote(entry.metric_availability?.memory_usage_mb, 'Memory metrics were unavailable this cycle.'))} | uptime=${formatPulseMetricWithNote(entry.uptime_seconds, 's', pulseMetricNote(entry.metric_availability?.uptime_seconds, 'Runtime uptime was unavailable this cycle.'))} | latency=${formatPulseMetricWithNote(entry.runtime_latency_ms, 'ms', pulseMetricNote(entry.metric_availability?.runtime_latency_ms, 'Latency was unavailable this cycle.'))} | token_metrics=${pulseMetricNote(entry.metric_availability?.token_metrics, 'Token metrics stay unavailable in metadata-only observation mode.')} | gpu_available=${String(entry.gpu_available)} | gpu_vram=${formatPulseMetric(entry.gpu_vram_used_mb, 'MB')} | gpu_util=${formatPulseMetric(entry.gpu_utilization_percent, '%')}`);
912
+ if (entry.degraded_reasons?.length) {
913
+ console.log(` degraded_reasons=${entry.degraded_reasons.join(' | ')}`);
914
+ }
915
+ }
916
+ }
917
+ function formatPulseRegistrationLabel(event) {
918
+ if (event.binding_status === 'bound')
919
+ return 'bound';
920
+ if (event.binding_status === 'pending')
921
+ return 'registered';
922
+ return event.registration_state;
923
+ }
924
+ function printPulseHistory(events, limit) {
925
+ console.log(`[forkit-connect] Runtime signals history showing=${events.length} limit=${limit}`);
926
+ console.log('[forkit-connect] History entries show best-effort runtime signals only, not billing records.');
927
+ for (const event of events) {
928
+ console.log(`- measured_at=${event.measured_at} | runtime=${event.runtime_name} | model=${event.model_name || 'n/a'} | registration=${formatPulseRegistrationLabel(event)} | observation_mode=${event.observation_mode || 'metadata_only'} | degraded_mode=${event.degraded_mode || 'normal'} | pulse_status=${event.pulse_status} | port_alive=${String(event.port_alive)} | process_alive=${String(event.process_alive)} | gpu_available=${String(event.gpu_available)} | gpu_vram=${formatPulseMetric(event.gpu_vram_used_mb, 'MB')} | gpu_util=${formatPulseMetric(event.gpu_utilization_percent, '%')}`);
929
+ console.log(` metrics: cpu=${formatPulseMetricWithNote(event.cpu_usage_percent, '%', pulseMetricNote(event.metric_availability?.cpu_usage_percent, 'CPU metrics were unavailable this cycle.'))} | memory=${formatPulseMetricWithNote(event.memory_usage_mb, 'MB', pulseMetricNote(event.metric_availability?.memory_usage_mb, 'Memory metrics were unavailable this cycle.'))} | uptime=${formatPulseMetricWithNote(event.uptime_seconds, 's', pulseMetricNote(event.metric_availability?.uptime_seconds, 'Runtime uptime was unavailable this cycle.'))} | latency=${formatPulseMetricWithNote(event.runtime_latency_ms, 'ms', pulseMetricNote(event.metric_availability?.runtime_latency_ms, 'Latency was unavailable this cycle.'))} | token_metrics=${pulseMetricNote(event.metric_availability?.token_metrics, 'Token metrics stay unavailable in metadata-only observation mode.')}`);
930
+ if (event.degraded_reasons?.length) {
931
+ console.log(` degraded_reasons=${event.degraded_reasons.join(' | ')}`);
932
+ }
933
+ }
934
+ }
935
+ function formatTimestamp(value) {
936
+ return typeof value === 'string' && value.trim() ? value : 'n/a';
937
+ }
938
+ function formatEvidenceDetails(details) {
939
+ const entries = Object.entries(details)
940
+ .filter(([, value]) => value !== null && value !== undefined && `${value}`.trim() !== '')
941
+ .slice(0, 4)
942
+ .map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
943
+ return entries.length > 0 ? ` | ${entries.join(' | ')}` : '';
944
+ }
945
+ function printCollectedChanges(service, limit) {
946
+ const state = service.getStateStore().readState();
947
+ const paths = service.getStateStore().getPaths();
948
+ const recentEvidence = [...state.evidence_events].slice(-limit).reverse();
949
+ const recentPulse = service.getPulseHistory(limit);
950
+ const evidenceTypeCounts = new Map();
951
+ for (const event of state.evidence_events) {
952
+ evidenceTypeCounts.set(event.type, (evidenceTypeCounts.get(event.type) || 0) + 1);
953
+ }
954
+ console.log('[forkit-connect] Collected changes');
955
+ console.log(`- state_dir=${paths.stateDir}`);
956
+ console.log(`- state_file=${paths.stateFile}`);
957
+ console.log(`- evidence_events=${state.evidence_events.length}`);
958
+ console.log(`- pulse_events=${state.pulse_events.length}`);
959
+ console.log(`- sync_queue=${state.sync_queue.length}`);
960
+ console.log(`- pending_reviews=${state.pending_reviews.length}`);
961
+ console.log(`- evolution_candidates=${state.evolution_candidates.length}`);
962
+ console.log(`- c2_events=${state.c2_events.length}`);
963
+ console.log(`- detected_models=${state.detected_models.length}`);
964
+ console.log(`- detected_runtimes=${state.detected_runtimes.length}`);
965
+ console.log(`- detected_agents=${state.detected_agents.length}`);
966
+ console.log('[forkit-connect] Some collected changes are local advisory telemetry and review evidence; only synced queue items become backend-authoritative.');
967
+ const sortedEvidenceTypes = [...evidenceTypeCounts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
968
+ if (sortedEvidenceTypes.length > 0) {
969
+ console.log('[forkit-connect] Evidence types observed:');
970
+ for (const [type, count] of sortedEvidenceTypes) {
971
+ console.log(`- ${type}: ${count}`);
972
+ }
973
+ }
974
+ if (recentEvidence.length > 0) {
975
+ console.log(`[forkit-connect] Recent evidence events showing=${recentEvidence.length} limit=${limit}`);
976
+ for (const event of recentEvidence) {
977
+ console.log(`- ${formatTimestamp(event.createdAt)} | ${event.type}${formatEvidenceDetails(event.details)}`);
978
+ }
979
+ }
980
+ if (recentPulse.length > 0) {
981
+ console.log(`[forkit-connect] Recent runtime signal history showing=${recentPulse.length} limit=${limit}`);
982
+ for (const event of recentPulse) {
983
+ console.log(`- ${event.measured_at} | runtime=${event.runtime_name} | model=${event.model_name || 'n/a'} | pulse=${event.pulse_status} | registration=${formatPulseRegistrationLabel(event)}`);
984
+ }
985
+ }
986
+ if (state.sync_queue.length > 0) {
987
+ console.log('[forkit-connect] Pending sync queue:');
988
+ for (const item of state.sync_queue.slice(0, limit)) {
989
+ console.log(`- ${item.type} | endpoint=${item.endpoint} | attempts=${item.attempts} | created_at=${item.createdAt} | next_retry_at=${item.nextRetryAt} | last_error=${item.lastError || 'n/a'}`);
990
+ }
991
+ }
992
+ }
993
+ function shortId(value, length = 10) {
994
+ if (!value)
995
+ return 'n/a';
996
+ return value.slice(0, length);
997
+ }
998
+ function formatReviewEvidence(entry) {
999
+ const hints = [];
1000
+ if (entry.confidence) {
1001
+ hints.push(`confidence=${entry.confidence}`);
1002
+ }
1003
+ if (entry.source_label) {
1004
+ hints.push(`source=${entry.source_label}`);
1005
+ }
1006
+ if (entry.matched_terms?.length) {
1007
+ hints.push(`matched=${entry.matched_terms.join(',')}`);
1008
+ }
1009
+ if (entry.reason) {
1010
+ hints.push(`reason=${entry.reason}`);
1011
+ }
1012
+ return hints.length > 0 ? ` | ${hints.join(' | ')}` : '';
1013
+ }
1014
+ function printReviewSnapshot(snapshot) {
1015
+ const order = [
1016
+ 'new_unregistered',
1017
+ 'known_unregistered',
1018
+ 'pending_draft',
1019
+ 'bound_passport',
1020
+ 'shadow_candidate',
1021
+ 'provider_unavailable',
1022
+ ];
1023
+ console.log(`[forkit-connect] Review items=${snapshot.total}`);
1024
+ for (const status of order) {
1025
+ const entries = snapshot.groups[status];
1026
+ if (entries.length === 0)
1027
+ continue;
1028
+ console.log(`${formatReviewStatusLabel(status)}: ${entries.length}`);
1029
+ for (const entry of entries) {
1030
+ console.log(`- item=${entry.model_name || entry.process_name || 'n/a'} | runtime=${entry.runtime_name || 'n/a'} | discovery=${shortId(entry.discovery_hash)} | registration=${shortId(entry.registration_key)} | next=${formatReviewActionLabel(entry.suggested_action)}${formatReviewEvidence(entry)}`);
1031
+ }
1032
+ }
1033
+ }
1034
+ function printC2Status(status) {
1035
+ console.log(JSON.stringify({
1036
+ authenticated: status.authenticated,
1037
+ bound: status.bound,
1038
+ runtimeSignal_key_configured: status.runtimeSignalKeyConfigured,
1039
+ bound_gaid: status.boundGaid,
1040
+ pending_events: status.pendingEvents,
1041
+ local_only_events: status.localOnlyEvents,
1042
+ synced_events: status.syncedEvents,
1043
+ last_sync_at: status.lastSyncAt,
1044
+ last_sync_error: status.lastSyncError,
1045
+ }, null, 2));
1046
+ }
1047
+ function printTrainStatus(status) {
1048
+ if (Array.isArray(status)) {
1049
+ console.log(JSON.stringify({
1050
+ total_build_sessions: status.length,
1051
+ sessions: status,
1052
+ }, null, 2));
1053
+ return;
1054
+ }
1055
+ console.log(JSON.stringify(status, null, 2));
1056
+ }
1057
+ function printAgentReview(snapshot) {
1058
+ const order = ['new_agent', 'known_agent', 'linked_agent', 'inactive_agent', 'unknown_ai_tool'];
1059
+ console.log(`[forkit-connect] Agent review items=${snapshot.total}`);
1060
+ for (const status of order) {
1061
+ const entries = snapshot.groups[status];
1062
+ if (entries.length === 0)
1063
+ continue;
1064
+ console.log(`${status}: ${entries.length}`);
1065
+ for (const entry of entries) {
1066
+ console.log(`- agent=${entry.agent_name} | type=${entry.agent_type} | status=${entry.status} | id=${shortId(entry.agent_id)} | linked_model=${entry.linked_model_name || 'n/a'} | passport=${entry.linked_passport_gaid || 'n/a'} | action=${entry.suggested_action}${formatReviewEvidence(entry)}`);
1067
+ }
1068
+ }
1069
+ }
1070
+ function printAgentStatus(status) {
1071
+ console.log(JSON.stringify({
1072
+ detected_agents: status.detectedAgents,
1073
+ connected_agents: status.connectedAgents,
1074
+ linked_agents: status.linkedAgents,
1075
+ unlinked_agents: status.unlinkedAgents,
1076
+ latest_agent_lifecycle_event: status.latestAgentLifecycleEvent,
1077
+ c2_sync_status: status.c2SyncStatus,
1078
+ }, null, 2));
1079
+ }
1080
+ function printAgentLedger(summary) {
1081
+ console.log(JSON.stringify(summary, null, 2));
1082
+ }
1083
+ function printRuntimeReview(summary) {
1084
+ console.log(`[forkit-connect] Runtime review items=${summary.total_runtimes}`);
1085
+ for (const runtime of summary.runtimes) {
1086
+ console.log(`- runtime=${runtime.runtime_name} | type=${runtime.runtime_type} | status=${runtime.status} | gaid=${shortId(runtime.runtime_gaid)} | linked_models=${runtime.linked_models_count} | linked_agents=${runtime.linked_agents_count} | health=${runtime.health_status} | next=${runtime.recommended_action}`);
1087
+ }
1088
+ }
1089
+ function printRuntimeStatus(status) {
1090
+ console.log(JSON.stringify({
1091
+ total_runtimes: status.total_runtimes,
1092
+ online_runtimes: status.online_runtimes,
1093
+ offline_runtimes: status.offline_runtimes,
1094
+ linked_runtimes: status.linked_runtimes,
1095
+ unlinked_runtimes: status.unlinked_runtimes,
1096
+ unhealthy_runtimes: status.unhealthy_runtimes,
1097
+ latest_runtime_lifecycle_event: status.latest_runtime_lifecycle_event,
1098
+ c2_pending_count: status.c2_pending_count,
1099
+ }, null, 2));
1100
+ }
1101
+ function formatSmartInboxActionValue(action, itemType, connectableModelName) {
1102
+ switch (action) {
1103
+ case 'connect_existing_passport':
1104
+ return 'Open Registered Passport';
1105
+ case 'create_passport_draft':
1106
+ if (itemType === 'agent')
1107
+ return 'Register Agent';
1108
+ if (itemType === 'runtime')
1109
+ return connectableModelName ? `Register ${connectableModelName}` : 'Register Runtime';
1110
+ return 'Register Model';
1111
+ case 'link_agent_to_model':
1112
+ return 'Register Agent';
1113
+ case 'confirm_version_candidate':
1114
+ return 'Review Version Candidate';
1115
+ case 'reconnect_account':
1116
+ return 'Reconnect Account';
1117
+ case 'open_on_forkit':
1118
+ return 'Open on Forkit.dev';
1119
+ case 'defer':
1120
+ return 'Ask Later';
1121
+ case 'ignore':
1122
+ return 'Do Not Register';
1123
+ default:
1124
+ return action.replaceAll('_', ' ');
1125
+ }
1126
+ }
1127
+ function formatSmartInboxActionLabel(item) {
1128
+ return formatSmartInboxActionValue(item.recommended_action, item.item_type, String(item.details_received_automatically.connectable_model_name || '').trim());
1129
+ }
1130
+ function printInboxGroup(label, items) {
1131
+ if (items.length === 0)
1132
+ return;
1133
+ console.log(`${label}: ${items.length}`);
1134
+ for (const item of items) {
1135
+ console.log(`- ${item.display_name} | type=${item.item_type} | confidence=${item.confidence} | next=${formatSmartInboxActionLabel(item)}${item.matched_passport_gaid ? ` | match=${shortId(item.matched_passport_gaid)}` : ''}`);
1136
+ }
1137
+ }
1138
+ function printSmartInbox(inbox) {
1139
+ console.log(`[forkit-connect] Smart Registration Inbox generated_at=${inbox.summary.generated_at}`);
1140
+ console.log(`Governance authority: website | user_api_key_required=${String(inbox.user_api_key_required)}`);
1141
+ printInboxGroup('Ready to Connect', inbox.groups.ready_to_connect);
1142
+ printInboxGroup('Needs Confirmation', inbox.groups.needs_confirmation);
1143
+ printInboxGroup('Connected', inbox.groups.connected);
1144
+ printInboxGroup('Ignored', inbox.groups.ignored);
1145
+ if (inbox.summary.next_recommended_action) {
1146
+ console.log(`Next recommended action: ${formatSmartInboxActionValue(inbox.summary.next_recommended_action)}`);
1147
+ }
1148
+ }
1149
+ function printConnectStatusOverview(status) {
1150
+ const sessionMissing = !status.device_paired && status.binding_state === 'active' && !status.credential_reconnect_needed;
1151
+ console.log(JSON.stringify({
1152
+ device_paired: status.device_paired,
1153
+ workspace_id: status.workspace_id,
1154
+ project_id: status.project_id,
1155
+ binding_state: status.binding_state,
1156
+ lifecycle_note: status.lifecycle_note,
1157
+ daemon_status: status.daemon_status,
1158
+ models_discovered: status.models_discovered,
1159
+ agents_discovered: status.agents_discovered,
1160
+ runtimes_discovered: status.runtimes_discovered,
1161
+ paused_session_count: status.paused_session_count,
1162
+ revoked_session_count: status.revoked_session_count,
1163
+ credential_reconnect_needed: status.credential_reconnect_needed,
1164
+ ready_to_connect_count: status.ready_to_connect_count,
1165
+ needs_confirmation_count: status.needs_confirmation_count,
1166
+ connected_count: status.connected_count,
1167
+ c2_sync_pending: status.c2_sync_pending,
1168
+ privacy_mode: status.privacy_mode,
1169
+ next_recommended_action: status.next_recommended_action,
1170
+ session_truth: sessionMissing ? 'missing_with_local_scope' : 'normal',
1171
+ }, null, 2));
1172
+ }
1173
+ function printPublicStatusOverview(status) {
1174
+ console.log('[forkit-connect] Status');
1175
+ console.log(`- device_paired=${String(status.device_paired)}`);
1176
+ console.log(`- workspace=${status.workspace_id || 'not selected'}`);
1177
+ console.log(`- project=${status.project_id || 'not selected'}`);
1178
+ console.log(`- daemon=${status.daemon_status}`);
1179
+ console.log(`- models_discovered=${status.models_discovered}`);
1180
+ console.log(`- agents_discovered=${status.agents_discovered}`);
1181
+ console.log(`- runtimes_discovered=${status.runtimes_discovered}`);
1182
+ console.log(`- ready_to_connect=${status.ready_to_connect_count}`);
1183
+ console.log(`- needs_confirmation=${status.needs_confirmation_count}`);
1184
+ console.log(`- connected=${status.connected_count}`);
1185
+ console.log(`- c2_sync_pending=${status.c2_sync_pending}`);
1186
+ console.log(`- privacy_mode=${status.privacy_mode}`);
1187
+ if (status.lifecycle_note) {
1188
+ console.log(`- note=${status.lifecycle_note}`);
1189
+ }
1190
+ if (!status.device_paired && status.binding_state === 'active') {
1191
+ console.log('- warning=Local workspace/project scope exists, but the account session is missing. Re-login is required before governed actions can continue.');
1192
+ }
1193
+ }
1194
+ function printPublicStatusGuidance(status, sessionState) {
1195
+ if (sessionState === 'missing') {
1196
+ console.log('- note=Local discovery is working. Sign in next to pair this device with your Forkit.dev account.');
1197
+ console.log('- next=run forkit-connect login');
1198
+ if (status.binding_state === 'active' && status.workspace_id && status.project_id) {
1199
+ console.log('- warning=This device has local workspace/project scope saved, but it is not an active account session yet.');
1200
+ }
1201
+ return;
1202
+ }
1203
+ if (sessionState === 'expired') {
1204
+ console.log('- note=Your local device is initialized, but the account session needs to be renewed.');
1205
+ console.log('- next=run forkit-connect login');
1206
+ return;
1207
+ }
1208
+ if (!status.device_paired) {
1209
+ console.log('- note=Device identity is ready locally, but browser approval still needs to complete.');
1210
+ console.log('- next=finish login approval on Forkit.dev, then rerun forkit-connect status');
1211
+ return;
1212
+ }
1213
+ if (status.daemon_status === 'stopped') {
1214
+ console.log('- note=Background sync is idle right now. This is expected until you start the local daemon.');
1215
+ console.log('- next=run forkit-connect start');
1216
+ return;
1217
+ }
1218
+ if (status.next_recommended_action) {
1219
+ console.log(`- next_detail=${formatSmartInboxActionValue(status.next_recommended_action)}`);
1220
+ }
1221
+ }
1222
+ async function checkBackendSessionState(service) {
1223
+ const sessionRefValue = String(service.readSessionRef() || '').trim();
1224
+ if (!sessionRefValue)
1225
+ return 'missing';
1226
+ const api = new api_1.ConnectApiClient({
1227
+ baseUrl: DEFAULT_BASE_URL,
1228
+ sessionRef: sessionRefValue,
1229
+ });
1230
+ const result = await api.getProfileAccess();
1231
+ if (result.ok)
1232
+ return 'authorized';
1233
+ if (result.status === 401 || result.status === 403)
1234
+ return 'expired';
1235
+ return 'unavailable';
1236
+ }
1237
+ function normalizeModelSelector(raw) {
1238
+ return raw.trim().toLowerCase();
1239
+ }
1240
+ function resolveSingleDetectedModelByName(service, rawName, digest) {
1241
+ const state = service.getStateStore().readState();
1242
+ const candidates = (state.detected_models || []).filter((item) => (item.status !== 'ignored'
1243
+ && normalizeModelSelector(item.model) === normalizeModelSelector(rawName)
1244
+ && (!digest || item.digest === digest)));
1245
+ if (candidates.length === 1) {
1246
+ return candidates[0];
1247
+ }
1248
+ return null;
1249
+ }
1250
+ async function promptSelection(label, options) {
1251
+ if (!options.length)
1252
+ return null;
1253
+ if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
1254
+ const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
1255
+ try {
1256
+ console.log(label);
1257
+ options.forEach((option, index) => {
1258
+ console.log(` ${index + 1}. ${option.label}`);
1259
+ });
1260
+ const answer = (await rl.question('Choose number: ')).trim();
1261
+ const index = Number(answer);
1262
+ if (!Number.isFinite(index) || index < 1 || index > options.length) {
1263
+ return null;
1264
+ }
1265
+ return options[index - 1]?.value ?? null;
1266
+ }
1267
+ finally {
1268
+ rl.close();
1269
+ }
1270
+ }
1271
+ (0, node_readline_1.emitKeypressEvents)(node_process_1.stdin);
1272
+ const canUseRawMode = typeof node_process_1.stdin.setRawMode === 'function';
1273
+ const previousRawMode = canUseRawMode ? node_process_1.stdin.isRaw : false;
1274
+ if (canUseRawMode) {
1275
+ node_process_1.stdin.setRawMode(true);
1276
+ }
1277
+ const maxVisibleOptions = 10;
1278
+ let selectedIndex = 0;
1279
+ let renderLineCount = 0;
1280
+ const dim = '\u001B[2m';
1281
+ const inverse = '\u001B[7m';
1282
+ const reset = '\u001B[0m';
1283
+ const hideCursor = '\u001B[?25l';
1284
+ const showCursor = '\u001B[?25h';
1285
+ const normalizedLabel = label.trim().endsWith(':') ? label.trim() : `${label.trim()}:`;
1286
+ const render = () => {
1287
+ if (renderLineCount > 0) {
1288
+ node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
1289
+ node_process_1.stdout.write('\u001B[0J');
1290
+ }
1291
+ const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisibleOptions / 2), Math.max(0, options.length - maxVisibleOptions)));
1292
+ const visibleOptions = options.slice(startIndex, startIndex + maxVisibleOptions);
1293
+ const endIndex = startIndex + visibleOptions.length;
1294
+ const lines = [
1295
+ normalizedLabel,
1296
+ '',
1297
+ ...(startIndex > 0 ? [`${dim} …${reset}`] : []),
1298
+ ...visibleOptions.map((option, offset) => {
1299
+ const absoluteIndex = startIndex + offset;
1300
+ if (absoluteIndex === selectedIndex) {
1301
+ return `> ${inverse}${option.label}${reset}`;
1302
+ }
1303
+ return ` ${option.label}`;
1304
+ }),
1305
+ ...(endIndex < options.length ? [`${dim} …${reset}`] : []),
1306
+ '',
1307
+ `${dim}↑/↓ move • Enter select • Esc cancel${reset}`,
1308
+ ];
1309
+ node_process_1.stdout.write(`${hideCursor}${lines.join('\n')}\n`);
1310
+ renderLineCount = lines.length;
1311
+ };
1312
+ render();
1313
+ // Drain any keypresses buffered during async operations (e.g. discovery scan).
1314
+ // Without this, keys pressed while loading fire immediately against the new listener
1315
+ // and cause phantom selections — the prompt appears to flash through 10–18 times.
1316
+ await new Promise((resolve) => setTimeout(resolve, 40));
1317
+ return await new Promise((resolve) => {
1318
+ const cleanup = (value) => {
1319
+ node_process_1.stdin.off('keypress', onKeypress);
1320
+ if (canUseRawMode) {
1321
+ node_process_1.stdin.setRawMode(previousRawMode === true);
1322
+ }
1323
+ node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
1324
+ node_process_1.stdout.write(`\u001B[0J${showCursor}`);
1325
+ if (value) {
1326
+ console.log(`${label}: ${options[selectedIndex]?.label || value}`);
1327
+ }
1328
+ resolve(value);
1329
+ };
1330
+ const moveSelection = (delta) => {
1331
+ selectedIndex = (selectedIndex + delta + options.length) % options.length;
1332
+ render();
1333
+ };
1334
+ const onKeypress = (_value, key) => {
1335
+ if (key.ctrl && key.name === 'c') {
1336
+ cleanup(null);
1337
+ return;
1338
+ }
1339
+ if (key.name === 'up' || key.name === 'k') {
1340
+ moveSelection(-1);
1341
+ return;
1342
+ }
1343
+ if (key.name === 'down' || key.name === 'j') {
1344
+ moveSelection(1);
1345
+ return;
1346
+ }
1347
+ if (key.name === 'return' || key.name === 'enter') {
1348
+ cleanup(options[selectedIndex]?.value ?? null);
1349
+ return;
1350
+ }
1351
+ if (key.name === 'escape' || key.name === 'q') {
1352
+ cleanup(null);
1353
+ }
1354
+ };
1355
+ node_process_1.stdin.on('keypress', onKeypress);
1356
+ });
1357
+ }
1358
+ async function promptText(question, options) {
1359
+ const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
1360
+ try {
1361
+ const answer = (await rl.question(question)).trim();
1362
+ if (!answer && options?.allowEmpty !== true) {
1363
+ return null;
1364
+ }
1365
+ return answer;
1366
+ }
1367
+ finally {
1368
+ rl.close();
1369
+ }
1370
+ }
1371
+ function buildCreateAccountUrl() {
1372
+ return `${DEFAULT_BASE_URL}/auth?returnTo=${encodeURIComponent('/connect')}`;
1373
+ }
1374
+ async function openUrlWithFallback(url, message) {
1375
+ const opened = await openUrl(url);
1376
+ if (opened) {
1377
+ console.log('[forkit-connect] Browser opened.');
1378
+ return;
1379
+ }
1380
+ if (message) {
1381
+ console.log(message);
1382
+ }
1383
+ console.log(url);
1384
+ }
1385
+ function printTrayStatus(status, menu) {
1386
+ console.log(JSON.stringify({
1387
+ daemon_running: status.daemonRunning,
1388
+ detected_models: status.detectedModels,
1389
+ detected_agents: status.detectedAgents,
1390
+ shadow_or_unconnected_count: status.shadowOrUnconnectedCount,
1391
+ pulse_status: status.pulseStatus,
1392
+ c2_pending_count: status.c2PendingCount,
1393
+ recommended_next_action: status.recommendedNextAction,
1394
+ recommended_next_step: status.recommendedNextAction === 'connect_model'
1395
+ ? 'Run `forkit-connect connect <model>` to prepare a Passport draft.'
1396
+ : status.recommendedNextAction === 'link_agent_model'
1397
+ ? 'Run `forkit-connect agent review` to connect an agent and link it to a model.'
1398
+ : status.recommendedNextAction === 'review_agent'
1399
+ ? 'Run `forkit-connect agent review` to inspect detected AI agents.'
1400
+ : status.recommendedNextAction === 'view_pulse_history'
1401
+ ? 'Run `forkit-connect pulse history --limit 10` to review recent runtime health.'
1402
+ : status.recommendedNextAction === 'sync_c2'
1403
+ ? 'Sign in first, then sync metadata-only lifecycle updates.'
1404
+ : null,
1405
+ status_color: status.statusColor,
1406
+ tray_menu: menu,
1407
+ }, null, 2));
1408
+ }
1409
+ async function run() {
1410
+ const args = process.argv.slice(2);
1411
+ const command = args[0];
1412
+ const service = new service_1.ConnectV1Service();
1413
+ const sessionRef = getArg('--session-ref');
1414
+ const workspaceId = getArg('--workspace');
1415
+ const projectId = getArg('--project');
1416
+ const descriptionArg = getArg('--description');
1417
+ const projectNameArg = getArg('--project-name');
1418
+ const projectDescriptionArg = getArg('--project-description');
1419
+ const modelName = getArg('--model');
1420
+ const draftId = getArg('--draft');
1421
+ const intervalSecondsArg = getArg('--interval-seconds');
1422
+ const limitArg = getArg('--limit');
1423
+ if (sessionRef !== null) {
1424
+ service.setSessionRef(sessionRef);
1425
+ }
1426
+ if (!command) {
1427
+ showUsage();
1428
+ return;
1429
+ }
1430
+ if (isHelpCommand(command)) {
1431
+ showUsage();
1432
+ return;
1433
+ }
1434
+ const runPublicConnectInit = () => {
1435
+ printConnectInit(service.initializeConnectIdentity());
1436
+ };
1437
+ const runPublicConnectStatus = async () => {
1438
+ const sessionState = await checkBackendSessionState(service);
1439
+ if (service.readSessionRef()) {
1440
+ try {
1441
+ await service.refreshEffectiveBinding();
1442
+ }
1443
+ catch {
1444
+ // Status remains useful with the last local binding snapshot.
1445
+ }
1446
+ }
1447
+ const overview = service.getConnectStatusOverview();
1448
+ const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1449
+ const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1450
+ const displayOverview = accountTrusted
1451
+ ? overview
1452
+ : {
1453
+ ...overview,
1454
+ workspace_id: null,
1455
+ project_id: null,
1456
+ lifecycle_note: localScopeCached
1457
+ ? 'Local workspace/project scope is cached on this device, but account login is required before governed actions can continue.'
1458
+ : 'Account login is required before governed actions can continue.',
1459
+ };
1460
+ if (hasFlag('--json')) {
1461
+ printJson({
1462
+ ...overview,
1463
+ workspace_id: accountTrusted ? overview.workspace_id : null,
1464
+ project_id: accountTrusted ? overview.project_id : null,
1465
+ session_state: sessionState,
1466
+ session_truth: accountTrusted ? 'account_verified_or_offline' : 'local_scope_cached_login_required',
1467
+ local_scope_cached: !accountTrusted && localScopeCached,
1468
+ binding_truth: accountTrusted ? 'account_binding_active' : 'account_login_required',
1469
+ });
1470
+ return;
1471
+ }
1472
+ printPublicStatusOverview(displayOverview);
1473
+ console.log(`- session=${sessionState}`);
1474
+ console.log(`- binding_truth=${accountTrusted ? 'account_binding_active' : 'account_login_required'}`);
1475
+ if (!accountTrusted && localScopeCached) {
1476
+ console.log('- local_scope_cached=true');
1477
+ }
1478
+ printPublicStatusGuidance(displayOverview, sessionState);
1479
+ };
1480
+ const runPublicConnectInbox = () => {
1481
+ const inbox = service.buildSmartRegistrationInbox();
1482
+ if (hasFlag('--json')) {
1483
+ printJson(inbox);
1484
+ return;
1485
+ }
1486
+ printSmartInbox(inbox);
1487
+ };
1488
+ const runPublicCollectedChanges = () => {
1489
+ const rawLimit = limitArg !== null ? Number(limitArg) : 20;
1490
+ const historyLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 20;
1491
+ const state = service.getStateStore().readState();
1492
+ const paths = service.getStateStore().getPaths();
1493
+ const recentEvidence = [...state.evidence_events].slice(-historyLimit).reverse();
1494
+ const recentPulse = service.getPulseHistory(historyLimit);
1495
+ const evidenceTypeCounts = Object.fromEntries([...state.evidence_events.reduce((map, event) => {
1496
+ map.set(event.type, (map.get(event.type) || 0) + 1);
1497
+ return map;
1498
+ }, new Map()).entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])));
1499
+ if (hasFlag('--json')) {
1500
+ printJson({
1501
+ state_paths: paths,
1502
+ summary: {
1503
+ evidence_events: state.evidence_events.length,
1504
+ pulse_events: state.pulse_events.length,
1505
+ sync_queue: state.sync_queue.length,
1506
+ pending_reviews: state.pending_reviews.length,
1507
+ evolution_candidates: state.evolution_candidates.length,
1508
+ c2_events: state.c2_events.length,
1509
+ detected_models: state.detected_models.length,
1510
+ detected_runtimes: state.detected_runtimes.length,
1511
+ detected_agents: state.detected_agents.length,
1512
+ },
1513
+ evidence_types: evidenceTypeCounts,
1514
+ recent_evidence_events: recentEvidence,
1515
+ recent_pulse_events: recentPulse,
1516
+ pending_sync_queue: state.sync_queue.slice(0, historyLimit),
1517
+ note: 'Local review evidence and runtime signals are best-effort local observations. Synced queue items are the backend-authoritative subset.',
1518
+ });
1519
+ return;
1520
+ }
1521
+ printCollectedChanges(service, historyLimit);
1522
+ };
1523
+ const runPublicScan = async () => {
1524
+ const ignoreModel = getArg('--ignore-model');
1525
+ const ignoreDigest = getArg('--ignore-digest');
1526
+ if (ignoreModel && ignoreDigest) {
1527
+ const ignored = service.markModelIgnored(ignoreModel, ignoreDigest);
1528
+ if (!ignored) {
1529
+ console.error(`[forkit-connect] Model not found for ignore: ${ignoreModel} ${ignoreDigest}`);
1530
+ process.exitCode = 2;
1531
+ return;
1532
+ }
1533
+ console.log(`[forkit-connect] Ignored model: ${ignoreModel} (${ignoreDigest})`);
1534
+ return;
1535
+ }
1536
+ const result = await service.runDiscoveryCycle();
1537
+ console.log(result.message);
1538
+ for (const runtime of result.runtimes) {
1539
+ const runtimeStatus = runtime.ok ? 'detected' : 'unavailable';
1540
+ const errorPart = runtime.error ? ` | error=${runtime.error}` : '';
1541
+ console.log(`- runtime=${runtime.runtime} | status=${runtimeStatus} | endpoint=${runtime.endpoint} | models=${runtime.modelsFound}${errorPart}`);
1542
+ }
1543
+ for (const model of result.models) {
1544
+ const modelIdPart = model.modelId ? ` | id=${model.modelId}` : '';
1545
+ console.log(`- ${model.model} | runtime=${model.runtime} | digest=${model.digest}${modelIdPart} | source=${model.sourceLabel} | endpoint=${model.sourceEndpoint}`);
1546
+ }
1547
+ console.log(`[forkit-connect] Detected new=${result.newModels} already_known=${result.alreadyKnownModels} duplicates_skipped=${result.duplicateDiscoveriesSkipped}`);
1548
+ printDiscoverySummary(result.summary);
1549
+ console.log(`[forkit-connect] Queue queued=${result.queue.queued} skipped_pending=${result.queue.skippedPending} skipped_bound=${result.queue.skippedBound} skipped_duplicates=${result.queue.skippedDuplicate}`);
1550
+ const heartbeatGaid = getArg('--heartbeat-gaid');
1551
+ const heartbeatKey = getArg('--heartbeat-key');
1552
+ if (heartbeatGaid && heartbeatKey) {
1553
+ const queueId = service.queueHeartbeatRuntimeSignal(heartbeatGaid, heartbeatKey);
1554
+ if (queueId) {
1555
+ console.log(`[forkit-connect] Queued heartbeat runtime signal: ${queueId}`);
1556
+ }
1557
+ }
1558
+ console.log(`[forkit-connect] Queue processed=${result.sync.processed} succeeded=${result.sync.succeeded} failed=${result.sync.failed}`);
1559
+ if (!result.ok) {
1560
+ process.exitCode = 2;
1561
+ }
1562
+ };
1563
+ const runPublicUpdateCheck = async () => {
1564
+ const updateCheck = await (0, update_1.checkForUpdates)();
1565
+ const lines = (0, update_1.formatUpdateCheckLines)(updateCheck);
1566
+ printUpdateCheckLines(lines, !updateCheck.ok || (updateCheck.ok && updateCheck.availability === 'required-update'));
1567
+ if (!updateCheck.ok || updateCheck.availability === 'required-update') {
1568
+ process.exitCode = 2;
1569
+ }
1570
+ };
1571
+ const runPublicLogout = () => {
1572
+ const currentSessionRef = String(service.readSessionRef() || '').trim();
1573
+ const hadEnvironmentSession = Boolean(String(process.env.FORKIT_CONNECT_SESSION_REF || '').trim());
1574
+ delete process.env.FORKIT_CONNECT_SESSION_REF;
1575
+ try {
1576
+ service.setSessionRef(null);
1577
+ console.log('[forkit-connect] Logged out. Local discovery remains available on this device.');
1578
+ if (hadEnvironmentSession) {
1579
+ console.log('[forkit-connect] The in-process fallback session was cleared for this run.');
1580
+ }
1581
+ return;
1582
+ }
1583
+ catch (error) {
1584
+ if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1585
+ if (currentSessionRef) {
1586
+ console.log('[forkit-connect] Logged out from the current interactive run.');
1587
+ console.log('[forkit-connect] If you previously exported a session in your shell, remove it there with:');
1588
+ console.log('unset FORKIT_CONNECT_SESSION_REF');
1589
+ return;
1590
+ }
1591
+ console.log('[forkit-connect] No stored session found.');
1592
+ console.log('[forkit-connect] If you previously exported a session in this shell, remove it with:');
1593
+ console.log('unset FORKIT_CONNECT_SESSION_REF');
1594
+ return;
1595
+ }
1596
+ throw error;
1597
+ }
1598
+ };
1599
+ const activateEnvironmentSessionFallback = async (token) => {
1600
+ process.env.FORKIT_CONNECT_SESSION_REF = token;
1601
+ service.initializeConnectIdentity();
1602
+ try {
1603
+ await service.refreshEffectiveBinding();
1604
+ }
1605
+ catch {
1606
+ // Keep the current process authenticated via environment fallback even if backend refresh is temporarily unavailable.
1607
+ }
1608
+ };
1609
+ const runPublicLogin = async () => {
1610
+ const noBrowser = hasFlag('--no-browser');
1611
+ const api = new api_1.ConnectApiClient({
1612
+ baseUrl: DEFAULT_BASE_URL,
1613
+ sessionRef: null,
1614
+ });
1615
+ const started = await api.startDeviceConnect(service.buildDeviceConnectStartPayload());
1616
+ if (!started.ok || !isDeviceConnectStartResponse(started.body)) {
1617
+ console.error(`[forkit-connect] Device login start failed (${started.status}).`);
1618
+ if (started.body) {
1619
+ console.error(typeof started.body === 'string' ? started.body : JSON.stringify(started.body));
1620
+ }
1621
+ process.exitCode = 2;
1622
+ return false;
1623
+ }
1624
+ const start = started.body;
1625
+ printDeviceLoginInstructions(start);
1626
+ if (!noBrowser && hasLinuxGuiSession()) {
1627
+ const opened = await openUrl(start.verification_url);
1628
+ if (opened) {
1629
+ console.log('[forkit-connect] Browser opened. Approve the device there, then return here.');
1630
+ }
1631
+ else {
1632
+ console.log('[forkit-connect] Browser could not be opened automatically. Use the URL above manually.');
1633
+ }
1634
+ }
1635
+ else if (!noBrowser && process.platform === 'linux') {
1636
+ console.log('[forkit-connect] Headless Linux session detected. Open the verification URL manually.');
1637
+ }
1638
+ const deadline = new Date(start.expires_at).getTime();
1639
+ const pollEveryMs = Math.max(1000, Math.floor(start.poll_interval_seconds * 1000));
1640
+ while (Date.now() < deadline) {
1641
+ await sleep(pollEveryMs);
1642
+ const polled = await api.pollDeviceConnect(start.device_code);
1643
+ if (isPollApproved(polled.body)) {
1644
+ try {
1645
+ service.setSessionRef(polled.body.connect_access_token);
1646
+ await service.refreshEffectiveBinding();
1647
+ const displayName = getSessionDisplayName(polled.body.connect_access_token);
1648
+ console.log(displayName
1649
+ ? `[forkit-connect] Login approved. Welcome, ${displayName}. Session credentials stored securely.`
1650
+ : '[forkit-connect] Login approved. Session credentials stored securely.');
1651
+ return true;
1652
+ }
1653
+ catch (error) {
1654
+ if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1655
+ await activateEnvironmentSessionFallback(polled.body.connect_access_token);
1656
+ printSessionExportFallback(polled.body.connect_access_token);
1657
+ const displayName = getSessionDisplayName(polled.body.connect_access_token);
1658
+ console.log(displayName
1659
+ ? `[forkit-connect] Welcome, ${displayName}. Continuing with an environment-backed session for this run.`
1660
+ : '[forkit-connect] Continuing with an environment-backed session for this run.');
1661
+ process.exitCode = 0;
1662
+ return true;
1663
+ }
1664
+ throw error;
1665
+ }
1666
+ }
1667
+ if (isPollPending(polled.body)) {
1668
+ continue;
1669
+ }
1670
+ if (polled.status === 410) {
1671
+ console.error('[forkit-connect] Device login expired before approval.');
1672
+ process.exitCode = 2;
1673
+ return false;
1674
+ }
1675
+ if (polled.status === 403) {
1676
+ console.error('[forkit-connect] Device login was denied.');
1677
+ process.exitCode = 2;
1678
+ return false;
1679
+ }
1680
+ if (polled.status === 409) {
1681
+ console.error('[forkit-connect] Device code already consumed. Start login again.');
1682
+ process.exitCode = 2;
1683
+ return false;
1684
+ }
1685
+ console.error(`[forkit-connect] Poll failed (${polled.status}).`);
1686
+ if (polled.body) {
1687
+ console.error(typeof polled.body === 'string' ? polled.body : JSON.stringify(polled.body));
1688
+ }
1689
+ process.exitCode = 2;
1690
+ return false;
1691
+ }
1692
+ console.error('[forkit-connect] Device login timed out. Start login again.');
1693
+ process.exitCode = 2;
1694
+ return false;
1695
+ };
1696
+ const handleWorkspaceError = (error) => {
1697
+ const message = error instanceof Error ? error.message : 'workspace_command_failed';
1698
+ if (message === 'NOT_AUTHENTICATED') {
1699
+ console.error('Not authenticated. Run forkit-connect login first.');
1700
+ }
1701
+ else if (message.startsWith('WORKSPACE_LIST_FAILED:')) {
1702
+ console.error(`Workspace listing failed (${message.split(':')[1]}).`);
1703
+ }
1704
+ else {
1705
+ console.error(message);
1706
+ }
1707
+ process.exitCode = 2;
1708
+ };
1709
+ const buildWorkspaceStatusPayload = async () => {
1710
+ const sessionState = await checkBackendSessionState(service);
1711
+ if (service.readSessionRef()) {
1712
+ try {
1713
+ await service.refreshEffectiveBinding();
1714
+ }
1715
+ catch {
1716
+ // Keep local scope view available.
1717
+ }
1718
+ }
1719
+ const overview = service.getConnectStatusOverview();
1720
+ const operatingMode = resolveOperatingMode(service);
1721
+ const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1722
+ const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1723
+ return {
1724
+ session_state: sessionState,
1725
+ operating_mode: operatingMode.mode,
1726
+ tier: operatingMode.tier,
1727
+ workspace_id: accountTrusted ? overview.workspace_id : null,
1728
+ project_id: accountTrusted ? overview.project_id : null,
1729
+ binding_state: accountTrusted ? overview.binding_state : 'login_required',
1730
+ lifecycle_note: accountTrusted
1731
+ ? overview.lifecycle_note
1732
+ : localScopeCached
1733
+ ? 'Local workspace/project scope is cached on this device, but account login is required before governed actions can continue.'
1734
+ : 'Account login is required before governed workspace/project actions can continue.',
1735
+ daemon_status: overview.daemon_status,
1736
+ ready_to_connect_count: overview.ready_to_connect_count,
1737
+ needs_confirmation_count: overview.needs_confirmation_count,
1738
+ connected_count: overview.connected_count,
1739
+ };
1740
+ };
1741
+ const renderInteractiveStatusScreen = async (sessionState) => {
1742
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
1743
+ const displayName = accountLimits?.displayName ?? getSessionDisplayName(service.readSessionRef());
1744
+ renderInteractiveScreen('Forkit Connect', {
1745
+ subtitle: displayName ? `Welcome back, ${displayName}` : 'Interactive overview',
1746
+ sections: buildInteractiveOverviewSections(service, sessionState, accountLimits),
1747
+ footerLines: ['Select an action below.'],
1748
+ });
1749
+ };
1750
+ const renderInteractiveChangesScreen = () => {
1751
+ renderInteractiveScreen('Collected Changes', {
1752
+ subtitle: 'Local evidence, signal history, and queued sync work',
1753
+ sections: buildInteractiveChangesSections(service, 12),
1754
+ footerLines: ['Local evidence is advisory until it syncs to Forkit.dev.'],
1755
+ });
1756
+ };
1757
+ const renderInteractiveWorkspaceScreen = async () => {
1758
+ const payload = await buildWorkspaceStatusPayload();
1759
+ renderInteractiveScreen('Workspace Scope', {
1760
+ subtitle: 'Current workspace and project selection',
1761
+ sections: [
1762
+ {
1763
+ title: 'Scope',
1764
+ lines: [
1765
+ shellLine('Mode', payload.operating_mode),
1766
+ shellLine('Tier', payload.tier || 'unknown'),
1767
+ shellLine('Workspace', formatScopeReferenceLabel(payload.workspace_id, 'workspace')),
1768
+ shellLine('Project', formatScopeReferenceLabel(payload.project_id, 'project')),
1769
+ shellLine('Session', payload.session_state),
1770
+ shellLine('Binding state', payload.binding_state),
1771
+ shellLine('Daemon', payload.daemon_status),
1772
+ ],
1773
+ },
1774
+ {
1775
+ title: 'Queue',
1776
+ lines: [
1777
+ shellLine('Ready to connect', payload.ready_to_connect_count),
1778
+ shellLine('Needs confirmation', payload.needs_confirmation_count),
1779
+ shellLine('Connected', payload.connected_count),
1780
+ ...(payload.lifecycle_note ? [shellLine('Note', payload.lifecycle_note)] : []),
1781
+ ],
1782
+ },
1783
+ ],
1784
+ footerLines: ['Select a workspace action below.'],
1785
+ });
1786
+ };
1787
+ const runInteractiveAutoRefresh = async () => {
1788
+ // Only run discovery and binding refresh when the user has a session.
1789
+ // Pre-login, these calls add significant startup latency with no user benefit.
1790
+ 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
+ }
1803
+ }
1804
+ };
1805
+ const runInteractiveWorkspaceMenu = async () => {
1806
+ while (true) {
1807
+ try {
1808
+ await renderInteractiveWorkspaceScreen();
1809
+ const workspaceAction = await promptSelection('Workspace actions', [
1810
+ { value: 'select', label: 'Select workspace and project' },
1811
+ { value: 'create', label: 'Create workspace' },
1812
+ { value: 'back', label: 'Back' },
1813
+ ]);
1814
+ if (!workspaceAction || workspaceAction === 'back') {
1815
+ return;
1816
+ }
1817
+ if (workspaceAction === 'select') {
1818
+ await runWorkspaceSelect();
1819
+ }
1820
+ else if (workspaceAction === 'create') {
1821
+ await runWorkspaceCreate();
1822
+ }
1823
+ if (!process.exitCode || process.exitCode === 0) {
1824
+ continue;
1825
+ }
1826
+ return;
1827
+ }
1828
+ catch (error) {
1829
+ handleWorkspaceError(error);
1830
+ return;
1831
+ }
1832
+ }
1833
+ };
1834
+ const ensureInteractiveWorkspaceScope = async () => {
1835
+ try {
1836
+ await service.refreshEffectiveBinding();
1837
+ }
1838
+ catch {
1839
+ // Use local binding when backend refresh is temporarily unavailable.
1840
+ }
1841
+ const operatingMode = resolveOperatingMode(service);
1842
+ const currentState = service.getStateStore().readState();
1843
+ const boundWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim();
1844
+ const boundProjectId = String(currentState.project_binding.projectId || '').trim();
1845
+ if (operatingMode.mode !== 'governed') {
1846
+ if (!boundWorkspaceId || !boundProjectId) {
1847
+ console.log('[forkit-connect] Solo mode active. Personal registration can continue without workspace/project scope.');
1848
+ }
1849
+ return true;
1850
+ }
1851
+ if (boundWorkspaceId && boundProjectId) {
1852
+ return true;
1853
+ }
1854
+ console.log('[forkit-connect] Governed scope is required before registration.');
1855
+ try {
1856
+ await runWorkspaceSelect();
1857
+ }
1858
+ catch (error) {
1859
+ handleWorkspaceError(error);
1860
+ return false;
1861
+ }
1862
+ if (process.exitCode && process.exitCode !== 0) {
1863
+ return false;
1864
+ }
1865
+ process.exitCode = 0;
1866
+ const refreshedState = service.getStateStore().readState();
1867
+ return Boolean(String(refreshedState.workspace_binding.workspaceId || '').trim()
1868
+ && String(refreshedState.project_binding.projectId || '').trim());
1869
+ };
1870
+ const resolveInteractiveModel = (selector) => {
1871
+ const state = service.getStateStore().readState();
1872
+ return state.detected_models.find((model) => model.discoveryHash === selector)
1873
+ ?? state.detected_models.find((model) => model.model === selector)
1874
+ ?? state.detected_models.find((model) => model.discoveryHash.startsWith(selector))
1875
+ ?? null;
1876
+ };
1877
+ const createWorkspaceInteractively = async (api) => {
1878
+ const workspaceName = await promptText('Workspace name: ');
1879
+ if (!workspaceName) {
1880
+ console.error('Workspace creation cancelled.');
1881
+ process.exitCode = 2;
1882
+ return null;
1883
+ }
1884
+ const workspaceDescription = await promptText('Workspace description (optional): ', { allowEmpty: true });
1885
+ const result = await api.createWorkspace({
1886
+ name: workspaceName,
1887
+ ...(workspaceDescription ? { description: workspaceDescription } : {}),
1888
+ });
1889
+ if (!result.ok || !isWorkspaceCreateResponse(result.body)) {
1890
+ console.error(`Workspace creation failed (${result.status}).`);
1891
+ process.exitCode = 2;
1892
+ return null;
1893
+ }
1894
+ const workspace = result.body.workspace && typeof result.body.workspace === 'object'
1895
+ ? result.body.workspace
1896
+ : null;
1897
+ if (!String(workspace?.id || '').trim()) {
1898
+ console.error('Workspace creation returned no workspace id.');
1899
+ process.exitCode = 2;
1900
+ return null;
1901
+ }
1902
+ await loadCliAccountLimits({ force: true }).catch(() => null);
1903
+ return workspace;
1904
+ };
1905
+ const chooseInteractiveWorkspaceProject = async () => {
1906
+ const operatingMode = ensureGovernedMode();
1907
+ if (!operatingMode) {
1908
+ return null;
1909
+ }
1910
+ const api = buildWorkspaceApi();
1911
+ const { workspaces } = await runWorkspaceList();
1912
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
1913
+ const displayName = accountLimits?.displayName ?? getSessionDisplayName(service.readSessionRef());
1914
+ let selectedWorkspace = null;
1915
+ const currentState = service.getStateStore().readState();
1916
+ const boundWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim();
1917
+ const boundProjectId = String(currentState.project_binding.projectId || '').trim();
1918
+ const boundWorkspace = boundWorkspaceId
1919
+ ? workspaces.find((workspace) => String(workspace.id || '').trim() === boundWorkspaceId) ?? null
1920
+ : null;
1921
+ const availableWorkspaceChoices = boundWorkspaceId
1922
+ ? workspaces.filter((workspace) => String(workspace.id || '').trim() !== boundWorkspaceId)
1923
+ : workspaces;
1924
+ if (workspaces.length === 0) {
1925
+ renderInteractiveScreen('Choose Workspace', {
1926
+ subtitle: displayName ? `Welcome, ${displayName}` : 'No existing workspaces yet',
1927
+ sections: [
1928
+ {
1929
+ title: 'Next Step',
1930
+ lines: [shellListLine('Create a workspace first, then create or choose a project inside it.')],
1931
+ },
1932
+ ...(accountLimits ? [{
1933
+ title: 'Account Limits',
1934
+ lines: [
1935
+ shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
1936
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
1937
+ ],
1938
+ }] : []),
1939
+ ],
1940
+ });
1941
+ selectedWorkspace = await createWorkspaceInteractively(api);
1942
+ if (!selectedWorkspace) {
1943
+ return null;
1944
+ }
1945
+ }
1946
+ else {
1947
+ renderInteractiveScreen('Choose Workspace', {
1948
+ subtitle: displayName ? `Welcome, ${displayName}` : 'Use an existing workspace or create a new one',
1949
+ sections: [
1950
+ ...(boundWorkspaceId && boundProjectId ? [{
1951
+ title: 'Current Scope',
1952
+ lines: [
1953
+ shellLine('Workspace', boundWorkspace ? summarizeWorkspaceLabel(boundWorkspace) : formatScopeReferenceLabel(boundWorkspaceId, 'workspace')),
1954
+ shellLine('Project', formatScopeReferenceLabel(boundProjectId, 'project')),
1955
+ shellListLine('You can keep this scope or switch to another workspace/project below.'),
1956
+ ],
1957
+ }] : []),
1958
+ {
1959
+ title: 'Available Workspaces',
1960
+ lines: availableWorkspaceChoices.slice(0, 8).map((workspace) => shellListLine(summarizeWorkspaceLabel(workspace))),
1961
+ },
1962
+ ...(accountLimits ? [{
1963
+ title: 'Account Limits',
1964
+ lines: [
1965
+ shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
1966
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
1967
+ ],
1968
+ }] : []),
1969
+ ],
1970
+ footerLines: ['Choose a workspace below.'],
1971
+ });
1972
+ const selectedWorkspaceId = await promptSelection('Choose workspace', [
1973
+ ...(boundWorkspace
1974
+ ? [{ value: boundWorkspaceId, label: `Keep current workspace · ${summarizeWorkspaceLabel(boundWorkspace)}` }]
1975
+ : []),
1976
+ ...availableWorkspaceChoices.map((workspace) => ({
1977
+ value: String(workspace.id || ''),
1978
+ label: summarizeWorkspaceLabel(workspace),
1979
+ })),
1980
+ { value: '__create_workspace__', label: 'Create new workspace' },
1981
+ { value: 'back', label: 'Back' },
1982
+ ]);
1983
+ if (!selectedWorkspaceId || selectedWorkspaceId === 'back') {
1984
+ return null;
1985
+ }
1986
+ if (selectedWorkspaceId === '__create_workspace__') {
1987
+ selectedWorkspace = await createWorkspaceInteractively(api);
1988
+ if (!selectedWorkspace) {
1989
+ return null;
1990
+ }
1991
+ }
1992
+ else {
1993
+ selectedWorkspace = workspaces.find((workspace) => String(workspace.id || '') === selectedWorkspaceId) ?? null;
1994
+ }
1995
+ }
1996
+ const workspaceIdValue = String(selectedWorkspace?.id || '').trim();
1997
+ const workspaceLabel = selectedWorkspace ? summarizeWorkspaceLabel(selectedWorkspace) : String(workspaceIdValue || 'Selected workspace').trim();
1998
+ if (!workspaceIdValue) {
1999
+ console.error('Workspace selection returned no workspace id.');
2000
+ process.exitCode = 2;
2001
+ return null;
2002
+ }
2003
+ const projectsResult = await api.getWorkspaceProjects(workspaceIdValue);
2004
+ if (!projectsResult.ok || !isWorkspaceProjectsResponse(projectsResult.body)) {
2005
+ console.error(`Project listing failed (${projectsResult.status}).`);
2006
+ process.exitCode = 2;
2007
+ return null;
2008
+ }
2009
+ const projects = Array.isArray(projectsResult.body.projects) ? projectsResult.body.projects : [];
2010
+ let selectedProject = null;
2011
+ const boundProject = boundWorkspaceId === workspaceIdValue && boundProjectId
2012
+ ? projects.find((project) => String(project.id || '').trim() === boundProjectId) ?? null
2013
+ : null;
2014
+ const availableProjectChoices = boundProject
2015
+ ? projects.filter((project) => String(project.id || '').trim() !== boundProjectId)
2016
+ : projects;
2017
+ if (projects.length === 0) {
2018
+ renderInteractiveScreen('Choose Project', {
2019
+ subtitle: workspaceLabel,
2020
+ sections: [
2021
+ {
2022
+ title: 'Next Step',
2023
+ lines: [shellListLine('This workspace has no projects yet, so create the first project now.')],
2024
+ },
2025
+ ...(accountLimits ? [{
2026
+ title: 'Account Limits',
2027
+ lines: [
2028
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
2029
+ ],
2030
+ }] : []),
2031
+ ],
2032
+ });
2033
+ selectedProject = await createProjectInWorkspace(api, workspaceIdValue, { interactivePrompt: true });
2034
+ if (!selectedProject?.id) {
2035
+ return null;
2036
+ }
2037
+ }
2038
+ else {
2039
+ renderInteractiveScreen('Choose Project', {
2040
+ subtitle: workspaceLabel,
2041
+ sections: [
2042
+ ...(boundWorkspaceId === workspaceIdValue && boundProjectId ? [{
2043
+ title: 'Current Scope',
2044
+ lines: [
2045
+ shellLine('Current project', boundProject ? summarizeProjectLabel(boundProject) : formatScopeReferenceLabel(boundProjectId, 'project')),
2046
+ shellListLine('You can keep this project or switch to another one below.'),
2047
+ ],
2048
+ }] : []),
2049
+ {
2050
+ title: 'Available Projects',
2051
+ lines: availableProjectChoices.slice(0, 8).map((project) => shellListLine(summarizeProjectLabel(project))),
2052
+ },
2053
+ ...(accountLimits ? [{
2054
+ title: 'Account Limits',
2055
+ lines: [
2056
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
2057
+ ],
2058
+ }] : []),
2059
+ ],
2060
+ footerLines: ['Choose a project below.'],
2061
+ });
2062
+ const selectedProjectId = await promptSelection('Choose project', [
2063
+ ...(boundProject
2064
+ ? [{ value: boundProjectId, label: `Keep current project · ${summarizeProjectLabel(boundProject)}` }]
2065
+ : []),
2066
+ ...availableProjectChoices.map((project) => ({
2067
+ value: String(project.id || ''),
2068
+ label: summarizeProjectLabel(project),
2069
+ })),
2070
+ { value: '__create_project__', label: 'Create new project' },
2071
+ { value: 'back', label: 'Back' },
2072
+ ]);
2073
+ if (!selectedProjectId || selectedProjectId === 'back') {
2074
+ return null;
2075
+ }
2076
+ if (selectedProjectId === '__create_project__') {
2077
+ selectedProject = await createProjectInWorkspace(api, workspaceIdValue, { interactivePrompt: true });
2078
+ if (!selectedProject?.id) {
2079
+ return null;
2080
+ }
2081
+ }
2082
+ else {
2083
+ selectedProject = projects.find((project) => String(project.id || '') === selectedProjectId) ?? null;
2084
+ }
2085
+ }
2086
+ const projectIdValue = String(selectedProject?.id || '').trim();
2087
+ const projectLabel = selectedProject ? summarizeProjectLabel(selectedProject) : String(projectIdValue || 'Selected project').trim();
2088
+ if (!projectIdValue) {
2089
+ console.error('Project selection returned no project id.');
2090
+ process.exitCode = 2;
2091
+ return null;
2092
+ }
2093
+ await service.bindWorkspaceProject(workspaceIdValue, projectIdValue);
2094
+ process.exitCode = 0;
2095
+ return {
2096
+ workspaceId: workspaceIdValue,
2097
+ projectId: projectIdValue,
2098
+ workspaceLabel,
2099
+ projectLabel,
2100
+ };
2101
+ };
2102
+ const loadInteractiveDraftPassportContext = async () => {
2103
+ const sessionRefValue = String(service.readSessionRef() || '').trim();
2104
+ if (!sessionRefValue) {
2105
+ return { drafts: [], passports: [] };
2106
+ }
2107
+ const api = buildWorkspaceApi();
2108
+ const [draftsResult, passportsResult] = await Promise.all([
2109
+ api.getPassportDrafts(),
2110
+ api.getPassportsMine(20),
2111
+ ]);
2112
+ const drafts = draftsResult.ok && isPassportDraftListResponse(draftsResult.body)
2113
+ ? Array.isArray(draftsResult.body.drafts) ? draftsResult.body.drafts : []
2114
+ : [];
2115
+ const passports = passportsResult.ok ? readPassportList(passportsResult.body) : [];
2116
+ return { drafts, passports };
2117
+ };
2118
+ const chooseInteractiveRegistrationScope = async (targetSelector) => {
2119
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
2120
+ if (!storedSessionRef) {
2121
+ console.error('Not authenticated. Run forkit-connect login first.');
2122
+ process.exitCode = 2;
2123
+ return null;
2124
+ }
2125
+ const targetModel = resolveInteractiveModel(targetSelector);
2126
+ if (!targetModel) {
2127
+ console.error('Model not found in local detected models. Run forkit-connect scan first.');
2128
+ process.exitCode = 2;
2129
+ return null;
2130
+ }
2131
+ const currentState = service.getStateStore().readState();
2132
+ const boundWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim();
2133
+ const boundProjectId = String(currentState.project_binding.projectId || '').trim();
2134
+ const workspacePrepared = boundWorkspaceId && boundProjectId;
2135
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
2136
+ const displayName = accountLimits?.displayName ?? getSessionDisplayName(service.readSessionRef());
2137
+ renderInteractiveScreen('Register Passport', {
2138
+ subtitle: targetModel.model,
2139
+ sections: [
2140
+ {
2141
+ title: 'Model',
2142
+ lines: [
2143
+ shellListLine(formatModelFriendlyExplanation(targetModel)),
2144
+ shellLine('Runtime', targetModel.runtimeName || targetModel.runtime),
2145
+ shellLine('Size', deriveModelSizeLabel(targetModel.sizeBytes)),
2146
+ ],
2147
+ },
2148
+ {
2149
+ title: 'Choose Scope',
2150
+ lines: [
2151
+ shellListLine('Solo — personal registration, visible only to you.'),
2152
+ shellListLine('Workspace — register under a workspace and project. You will choose or create them next.'),
2153
+ ],
2154
+ },
2155
+ ...(accountLimits ? [{
2156
+ title: 'Account Limits',
2157
+ lines: [
2158
+ shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
2159
+ shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
2160
+ ],
2161
+ }] : []),
2162
+ ],
2163
+ footerLines: ['Choose Solo or Workspace below.'],
2164
+ });
2165
+ const scopeChoice = await promptSelection('Choose registration mode', [
2166
+ { value: 'solo', label: 'Solo — personal registration on this device' },
2167
+ { value: 'workspace', label: 'Workspace — select existing or create new workspace / project' },
2168
+ { value: 'back', label: 'Back' },
2169
+ ]);
2170
+ if (!scopeChoice || scopeChoice === 'back') {
2171
+ return null;
2172
+ }
2173
+ let workspaceSelection = null;
2174
+ if (scopeChoice === 'workspace') {
2175
+ workspaceSelection = await chooseInteractiveWorkspaceProject();
2176
+ if (!workspaceSelection) {
2177
+ return null;
2178
+ }
2179
+ }
2180
+ const registrationContext = await loadInteractiveDraftPassportContext();
2181
+ renderInteractiveScreen('Choose Draft Or Passport', {
2182
+ subtitle: displayName ? `Welcome, ${displayName}` : targetModel.model,
2183
+ sections: [
2184
+ {
2185
+ title: 'Existing Drafts',
2186
+ lines: registrationContext.drafts.length
2187
+ ? registrationContext.drafts.slice(0, 3).map((draft) => shellListLine(formatDraftLine(draft).replace(/^-\s*/, '')))
2188
+ : [shellListLine('No existing drafts found for this account right now.')],
2189
+ },
2190
+ {
2191
+ title: 'Existing Passports',
2192
+ lines: registrationContext.passports.length
2193
+ ? registrationContext.passports.slice(0, 3).map((passport) => shellListLine(formatPassportLine(passport).replace(/^-\s*/, '')))
2194
+ : [shellListLine('No existing published passports found for this account right now.')],
2195
+ },
2196
+ ...(accountLimits ? [{
2197
+ title: 'Account Limits',
2198
+ lines: [
2199
+ shellLine('Drafts', formatRemainingLimit(accountLimits.draftLimit, accountLimits.draftsUsed, 'draft')),
2200
+ shellLine('Private passports', formatRemainingLimit(accountLimits.privatePassportsLimit, accountLimits.privatePassportsUsed, 'private passport')),
2201
+ ],
2202
+ }] : []),
2203
+ ],
2204
+ footerLines: ['Choose whether to stop at a draft or try to publish a passport now.'],
2205
+ });
2206
+ const destinationChoice = await promptSelection('Choose destination', [
2207
+ { value: 'draft', label: 'Draft · save progress first and review later' },
2208
+ { value: 'passport', label: 'Passport · finish now when public publishing is allowed' },
2209
+ { value: 'back', label: 'Back' },
2210
+ ]);
2211
+ if (!destinationChoice || destinationChoice === 'back') {
2212
+ return null;
2213
+ }
2214
+ renderInteractiveScreen('Choose Visibility', {
2215
+ subtitle: displayName ? `Welcome, ${displayName}` : targetModel.model,
2216
+ sections: [
2217
+ {
2218
+ title: 'Visibility',
2219
+ lines: [
2220
+ shellListLine('Private keeps the registration as a draft for your review.'),
2221
+ shellListLine('Public can publish a passport immediately when your account and scope allow it.'),
2222
+ ],
2223
+ },
2224
+ ...(accountLimits ? [{
2225
+ title: 'Account Limits',
2226
+ lines: [
2227
+ shellLine('Private passports', formatRemainingLimit(accountLimits.privatePassportsLimit, accountLimits.privatePassportsUsed, 'private passport')),
2228
+ shellLine('Drafts', formatRemainingLimit(accountLimits.draftLimit, accountLimits.draftsUsed, 'draft')),
2229
+ ],
2230
+ }] : []),
2231
+ ],
2232
+ footerLines: ['Choose private or public below.'],
2233
+ });
2234
+ const visibilityChoice = await promptSelection('Choose visibility', [
2235
+ { value: 'private', label: 'Private · keep it draft-only for now' },
2236
+ { value: 'public', label: 'Public · allow immediate publishing when possible' },
2237
+ { value: 'back', label: 'Back' },
2238
+ ]);
2239
+ if (!visibilityChoice || visibilityChoice === 'back') {
2240
+ return null;
2241
+ }
2242
+ return {
2243
+ targetSelector: targetModel.discoveryHash,
2244
+ targetModel,
2245
+ scopeMode: scopeChoice === 'workspace' ? 'workspace' : 'solo',
2246
+ destination: destinationChoice === 'passport' ? 'passport' : 'draft',
2247
+ visibility: visibilityChoice === 'private' ? 'private' : 'public',
2248
+ workspaceId: workspaceSelection?.workspaceId ?? null,
2249
+ projectId: workspaceSelection?.projectId ?? null,
2250
+ workspaceLabel: workspaceSelection?.workspaceLabel ?? null,
2251
+ projectLabel: workspaceSelection?.projectLabel ?? null,
2252
+ };
2253
+ };
2254
+ const prepareInteractiveRegistrationScope = async (plan) => {
2255
+ const currentState = service.getStateStore().readState();
2256
+ const previousWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim() || null;
2257
+ const previousProjectId = String(currentState.project_binding.projectId || '').trim() || null;
2258
+ if (plan.scopeMode === 'solo') {
2259
+ await service.bindWorkspaceProject(null, null);
2260
+ return {
2261
+ ok: true,
2262
+ restore: async () => {
2263
+ if (previousWorkspaceId && previousProjectId) {
2264
+ await service.bindWorkspaceProject(previousWorkspaceId, previousProjectId);
2265
+ }
2266
+ },
2267
+ };
2268
+ }
2269
+ const scopeReady = Boolean(String(plan.workspaceId || '').trim() && String(plan.projectId || '').trim());
2270
+ return {
2271
+ ok: scopeReady,
2272
+ restore: async () => { },
2273
+ };
2274
+ };
2275
+ const runRegisterDetectedModel = async (plan) => {
2276
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
2277
+ if (!storedSessionRef) {
2278
+ console.error('Not authenticated. Run forkit-connect login first.');
2279
+ process.exitCode = 2;
2280
+ return false;
2281
+ }
2282
+ const preparedScope = await prepareInteractiveRegistrationScope(plan);
2283
+ if (!preparedScope.ok) {
2284
+ if (!process.exitCode || process.exitCode === 0) {
2285
+ process.exitCode = 2;
2286
+ }
2287
+ return false;
2288
+ }
2289
+ try {
2290
+ const result = await service.connectDetectedModel(plan.targetSelector, {
2291
+ visibility: plan.visibility,
2292
+ destination: plan.destination,
2293
+ });
2294
+ process.exitCode = 0;
2295
+ const renderResult = async (status, done) => {
2296
+ const websiteUrl = result.gaid
2297
+ ? buildPassportWebsiteUrl(result.gaid)
2298
+ : result.draftId
2299
+ ? buildDraftWebsiteUrl(result.draftId)
2300
+ : `${DEFAULT_BASE_URL}/connect`;
2301
+ const websiteLabel = result.gaid
2302
+ ? 'Open published passport on Forkit.dev'
2303
+ : result.draftId
2304
+ ? 'Open draft on Forkit.dev'
2305
+ : 'Open Forkit Connect on Forkit.dev';
2306
+ // Auto-open the website when registration succeeds — redirect user to their passport or draft
2307
+ if (done && websiteUrl) {
2308
+ await openUrlWithFallback(websiteUrl, `[forkit-connect] View on Forkit.dev: ${websiteUrl}`);
2309
+ }
2310
+ renderInteractiveScreen('Registration Result', {
2311
+ subtitle: result.model.model,
2312
+ sections: [
2313
+ {
2314
+ title: 'Your Choices',
2315
+ lines: [
2316
+ shellLine('Scope', plan.scopeMode),
2317
+ shellLine('Workspace', plan.workspaceLabel || 'solo'),
2318
+ shellLine('Project', plan.projectLabel || 'solo'),
2319
+ shellLine('Draft or passport', plan.destination),
2320
+ shellLine('Visibility', plan.visibility),
2321
+ ],
2322
+ },
2323
+ {
2324
+ title: 'Result',
2325
+ lines: [
2326
+ shellLine('Passport registered successfully', done ? 'yes' : 'no'),
2327
+ shellLine('Status', status),
2328
+ ...(result.draftId ? [shellLine('Draft ID', result.draftId)] : []),
2329
+ ...(result.gaid ? [shellLine('Passport GAID', result.gaid)] : []),
2330
+ shellLine('Website link', websiteUrl),
2331
+ ],
2332
+ },
2333
+ ],
2334
+ footerLines: ['Open the Forkit.dev page or go back.'],
2335
+ });
2336
+ const selected = await promptSelection('Registration result', [
2337
+ { value: 'open', label: websiteLabel },
2338
+ { value: 'back', label: 'Back' },
2339
+ ]);
2340
+ if (selected === 'open') {
2341
+ await openUrlWithFallback(websiteUrl, '[forkit-connect] Open this URL manually:');
2342
+ }
2343
+ };
2344
+ if (result.action === 'already_bound') {
2345
+ await renderResult('already connected to a passport', true);
2346
+ return true;
2347
+ }
2348
+ if (result.action === 'already_pending') {
2349
+ await renderResult('already has a pending draft', true);
2350
+ return true;
2351
+ }
2352
+ if (result.action === 'passport_registered') {
2353
+ await renderResult('passport published', true);
2354
+ return true;
2355
+ }
2356
+ if (result.action === 'draft_created') {
2357
+ await renderResult('draft created', true);
2358
+ return true;
2359
+ }
2360
+ await renderResult('draft queued locally', false);
2361
+ return true;
2362
+ }
2363
+ catch (error) {
2364
+ const message = error instanceof Error ? error.message : 'connect_failed';
2365
+ if (message === 'MODEL_NOT_FOUND') {
2366
+ console.error('Model not found in local detected models. Run forkit-connect scan first.');
2367
+ }
2368
+ else if (message === 'MODEL_SELECTION_AMBIGUOUS') {
2369
+ console.error('Model selection is ambiguous. Use the full discoveryHash.');
2370
+ }
2371
+ else if (message === 'WORKSPACE_PROJECT_BINDING_REQUIRED') {
2372
+ console.error('No governed workspace/project scope is linked yet. Choose a workspace/project first, then try again.');
2373
+ }
2374
+ else if (message === 'DRAFT_CREATION_NOT_ALLOWED_BY_BINDING') {
2375
+ console.error('Draft creation is not active for this binding yet. Complete Connect approval or update consent on Forkit.dev first.');
2376
+ }
2377
+ else if (message === 'DRAFT_ALREADY_EXISTS') {
2378
+ console.error('A backend draft already exists for this model. No duplicate draft created.');
2379
+ }
2380
+ else if (message === 'SIMILAR_PASSPORT_EXISTS') {
2381
+ console.error('A similar or existing passport already exists in your Forkit.dev account. Open inbox and review the connected/passport match before creating a new draft.');
2382
+ }
2383
+ else if (message.startsWith('DRAFT_CREATE_FAILED:')) {
2384
+ console.error(`[forkit-connect] Draft creation failed (${message.split(':')[1]}).`);
2385
+ }
2386
+ else {
2387
+ console.error(message);
2388
+ }
2389
+ process.exitCode = 2;
2390
+ return false;
2391
+ }
2392
+ finally {
2393
+ await preparedScope.restore();
2394
+ }
2395
+ };
2396
+ const buildInteractiveRegisterCandidates = () => {
2397
+ const inbox = service.buildSmartRegistrationInbox();
2398
+ const candidates = new Map();
2399
+ const groups = ['needs_confirmation', 'ready_to_connect'];
2400
+ const state = service.getStateStore().readState();
2401
+ for (const group of groups) {
2402
+ for (const item of inbox.groups[group]) {
2403
+ if (item.recommended_action !== 'create_passport_draft') {
2404
+ continue;
2405
+ }
2406
+ const model = item.item_type === 'model'
2407
+ ? state.detected_models.find((entry) => entry.discoveryHash === extractInboxItemSelector(item))
2408
+ ?? resolveSingleDetectedModelByName(service, item.display_name)
2409
+ : item.item_type === 'runtime'
2410
+ ? resolveSingleDetectedModelByName(service, String(item.details_received_automatically.connectable_model_name || '').trim())
2411
+ : null;
2412
+ if (!model) {
2413
+ continue;
2414
+ }
2415
+ const key = normalizeModelSelector(model.discoveryHash);
2416
+ if (candidates.has(key)) {
2417
+ continue;
2418
+ }
2419
+ candidates.set(key, {
2420
+ value: model.discoveryHash,
2421
+ label: `${model.model} · ${group === 'needs_confirmation' ? 'Needs confirmation' : 'Ready to connect'}`,
2422
+ preview: formatModelFriendlySummary(model),
2423
+ });
2424
+ }
2425
+ }
2426
+ return [...candidates.values()].sort((left, right) => left.label.localeCompare(right.label));
2427
+ };
2428
+ const runInteractiveRegisterMenu = async () => {
2429
+ await runInteractiveAutoRefresh();
2430
+ const candidates = buildInteractiveRegisterCandidates();
2431
+ if (candidates.length === 0) {
2432
+ renderInteractiveScreen('Register Models', {
2433
+ subtitle: 'No ready local models are available yet',
2434
+ sections: [
2435
+ {
2436
+ title: 'Next Steps',
2437
+ lines: [
2438
+ shellListLine('Run discovery to detect local runtimes and models.'),
2439
+ shellListLine('Review inbox items that still need confirmation.'),
2440
+ ],
2441
+ },
2442
+ ],
2443
+ });
2444
+ return;
2445
+ }
2446
+ renderInteractiveScreen('Register Models', {
2447
+ subtitle: 'Choose a ready local model to register',
2448
+ sections: [
2449
+ {
2450
+ title: 'Candidates',
2451
+ lines: candidates.slice(0, 8).map((candidate) => shellListLine(`${candidate.label} | ${candidate.preview}`)),
2452
+ },
2453
+ ],
2454
+ footerLines: ['Select a model below.'],
2455
+ });
2456
+ const selected = await promptSelection('Choose a model to register', [
2457
+ ...candidates,
2458
+ { value: '__all__', label: 'Register every ready model' },
2459
+ { value: '__back__', label: 'Back to main menu' },
2460
+ ]);
2461
+ if (!selected || selected === '__back__') {
2462
+ return;
2463
+ }
2464
+ if (selected === '__all__') {
2465
+ for (const candidate of candidates) {
2466
+ const plan = await chooseInteractiveRegistrationScope(candidate.value);
2467
+ if (!plan) {
2468
+ continue;
2469
+ }
2470
+ await runRegisterDetectedModel(plan);
2471
+ }
2472
+ return;
2473
+ }
2474
+ const plan = await chooseInteractiveRegistrationScope(selected);
2475
+ if (!plan) {
2476
+ return;
2477
+ }
2478
+ await runRegisterDetectedModel(plan);
2479
+ };
2480
+ const formatInboxGroupName = (group) => {
2481
+ switch (group) {
2482
+ case 'ready_to_connect':
2483
+ return 'Ready';
2484
+ case 'needs_confirmation':
2485
+ return 'Needs confirmation';
2486
+ case 'connected':
2487
+ return 'Connected';
2488
+ case 'ignored':
2489
+ return 'Ignored';
2490
+ default:
2491
+ return group.replaceAll('_', ' ');
2492
+ }
2493
+ };
2494
+ const extractInboxItemSelector = (item) => (item.item_id.split(':').slice(1).join(':'));
2495
+ const getInboxDetailText = (item, key) => String(item.details_received_automatically[key] || '').trim();
2496
+ const printInteractiveInboxItemDetails = (item, group) => {
2497
+ console.log(`[forkit-connect] Inbox item: ${item.display_name}`);
2498
+ console.log(`- group=${formatInboxGroupName(group)}`);
2499
+ console.log(`- type=${item.item_type}`);
2500
+ console.log(`- source=${item.detected_source}`);
2501
+ console.log(`- confidence=${item.confidence}`);
2502
+ console.log(`- next=${formatSmartInboxActionLabel(item)}`);
2503
+ if (item.workspaceId) {
2504
+ console.log(`- workspace=${item.workspaceId}`);
2505
+ }
2506
+ if (item.projectId) {
2507
+ console.log(`- project=${item.projectId}`);
2508
+ }
2509
+ if (item.passport_gaid) {
2510
+ console.log(`- passport_gaid=${item.passport_gaid}`);
2511
+ }
2512
+ if (item.matched_passport_gaid && item.matched_passport_gaid !== item.passport_gaid) {
2513
+ console.log(`- matched_passport=${item.matched_passport_gaid}`);
2514
+ }
2515
+ const detailKeys = [
2516
+ 'verification_summary',
2517
+ 'limitations_summary',
2518
+ 'scope_suggestion',
2519
+ 'scope_mismatch_reason',
2520
+ 'connectable_model_name',
2521
+ 'linked_model_candidate',
2522
+ 'public_state',
2523
+ 'last_seen',
2524
+ ];
2525
+ for (const key of detailKeys) {
2526
+ const value = getInboxDetailText(item, key);
2527
+ if (value) {
2528
+ console.log(`- ${key}=${value}`);
2529
+ }
2530
+ }
2531
+ };
2532
+ const runInteractiveOpenOnForkit = async (item) => {
2533
+ const gaid = item.passport_gaid || item.matched_passport_gaid;
2534
+ if (gaid) {
2535
+ console.log(`[forkit-connect] Review this item on Forkit.dev. Passport GAID: ${gaid}`);
2536
+ }
2537
+ else {
2538
+ console.log('[forkit-connect] Review this item on Forkit.dev.');
2539
+ }
2540
+ await openUrlWithFallback(`${DEFAULT_BASE_URL}/connect`, '[forkit-connect] Open this URL manually:');
2541
+ };
2542
+ const runInteractiveIgnoreInboxItem = (item) => {
2543
+ const selector = extractInboxItemSelector(item);
2544
+ if (item.item_type === 'model') {
2545
+ const state = service.getStateStore().readState();
2546
+ const model = state.detected_models.find((entry) => entry.discoveryHash === selector);
2547
+ if (!model) {
2548
+ console.error('Model not found anymore. Run forkit-connect scan again.');
2549
+ process.exitCode = 2;
2550
+ return;
2551
+ }
2552
+ const ignored = service.markModelIgnored(model.model, model.digest);
2553
+ if (!ignored) {
2554
+ console.error('Ignore failed.');
2555
+ process.exitCode = 2;
2556
+ return;
2557
+ }
2558
+ console.log(`[forkit-connect] Ignored model: ${model.model} (${model.digest})`);
2559
+ process.exitCode = 0;
2560
+ return;
2561
+ }
2562
+ if (item.item_type === 'runtime') {
2563
+ service.markRuntimeIgnored(selector);
2564
+ console.log(`[forkit-connect] Ignored runtime: ${item.display_name}`);
2565
+ process.exitCode = 0;
2566
+ return;
2567
+ }
2568
+ console.log('[forkit-connect] Ignore is not available for this inbox item type from the public menu yet.');
2569
+ };
2570
+ const runInteractiveDeferInboxItem = (item) => {
2571
+ const selector = extractInboxItemSelector(item);
2572
+ if (item.item_type === 'model') {
2573
+ service.deferDetectedModel(selector);
2574
+ console.log(`[forkit-connect] Deferred model review for: ${item.display_name}`);
2575
+ process.exitCode = 0;
2576
+ return;
2577
+ }
2578
+ if (item.item_type === 'runtime') {
2579
+ service.deferRuntimeSuggestion(selector);
2580
+ console.log(`[forkit-connect] Deferred runtime review for: ${item.display_name}`);
2581
+ process.exitCode = 0;
2582
+ return;
2583
+ }
2584
+ console.log('[forkit-connect] Defer is not available for this inbox item type from the public menu yet.');
2585
+ };
2586
+ const runInteractiveInboxAction = async (item, group) => {
2587
+ if (group === 'connected' && (item.item_type === 'model' || item.item_type === 'runtime')) {
2588
+ renderInteractiveScreen('Connected Model Activity', {
2589
+ subtitle: item.display_name,
2590
+ sections: buildConnectedItemActivitySections(service, item),
2591
+ footerLines: ['Forkit Connect keeps collecting local activity logs while background sync is running.'],
2592
+ });
2593
+ await promptSelection('Connected activity', [{ value: 'back', label: 'Back to inbox' }]);
2594
+ return;
2595
+ }
2596
+ const selected = await promptSelection('Choose an action for this inbox item', [
2597
+ ...item.allowed_actions.map((action) => ({
2598
+ value: action,
2599
+ label: formatSmartInboxActionValue(action, item.item_type, String(item.details_received_automatically.connectable_model_name || '').trim()),
2600
+ })),
2601
+ { value: 'back', label: 'Back to inbox' },
2602
+ ]);
2603
+ if (!selected || selected === 'back') {
2604
+ return;
2605
+ }
2606
+ if (selected === 'create_passport_draft') {
2607
+ const targetModelName = item.item_type === 'runtime'
2608
+ ? String(item.details_received_automatically.connectable_model_name || '').trim()
2609
+ : item.item_type === 'model'
2610
+ ? item.display_name
2611
+ : '';
2612
+ if (!targetModelName) {
2613
+ console.log('[forkit-connect] Direct registration from this inbox item type is not exposed in the public menu yet.');
2614
+ if (item.item_type === 'agent') {
2615
+ console.log('[forkit-connect] Use forkit-connect agent review for now.');
2616
+ }
2617
+ return;
2618
+ }
2619
+ const targetModel = resolveSingleDetectedModelByName(service, targetModelName);
2620
+ const plan = await chooseInteractiveRegistrationScope(targetModel?.discoveryHash || targetModelName);
2621
+ if (!plan) {
2622
+ return;
2623
+ }
2624
+ await runRegisterDetectedModel(plan);
2625
+ return;
2626
+ }
2627
+ if (selected === 'reconnect_account') {
2628
+ await runPublicLogin();
2629
+ return;
2630
+ }
2631
+ if (selected === 'ignore') {
2632
+ runInteractiveIgnoreInboxItem(item);
2633
+ return;
2634
+ }
2635
+ if (selected === 'defer') {
2636
+ runInteractiveDeferInboxItem(item);
2637
+ return;
2638
+ }
2639
+ if (selected === 'open_on_forkit' || selected === 'connect_existing_passport') {
2640
+ await runInteractiveOpenOnForkit(item);
2641
+ return;
2642
+ }
2643
+ if (selected === 'link_agent_to_model') {
2644
+ console.log('[forkit-connect] Agent-to-model linking is not yet exposed as a guided public CLI flow.');
2645
+ console.log('[forkit-connect] Use forkit-connect agent review for now.');
2646
+ return;
2647
+ }
2648
+ if (selected === 'confirm_version_candidate') {
2649
+ printJson(service.reviewModelEvolution());
2650
+ return;
2651
+ }
2652
+ };
2653
+ const runInteractiveInbox = async () => {
2654
+ await runInteractiveAutoRefresh();
2655
+ const groupOrder = ['needs_confirmation', 'ready_to_connect', 'connected', 'ignored'];
2656
+ while (true) {
2657
+ const inbox = service.buildSmartRegistrationInbox();
2658
+ const entries = groupOrder.flatMap((group) => inbox.groups[group].map((item) => ({ group, item })));
2659
+ if (entries.length === 0) {
2660
+ renderInteractiveScreen('Smart Registration Inbox', {
2661
+ subtitle: 'No inbox items are available right now',
2662
+ sections: [
2663
+ {
2664
+ title: 'Next Steps',
2665
+ lines: [shellListLine('Run discovery first to refresh local runtimes, models, and agents.')],
2666
+ },
2667
+ ],
2668
+ });
2669
+ return;
2670
+ }
2671
+ renderInteractiveScreen('Smart Registration Inbox', {
2672
+ subtitle: `Generated at ${inbox.summary.generated_at}`,
2673
+ sections: buildInteractiveInboxSections(inbox),
2674
+ footerLines: ['Choose an inbox item below.'],
2675
+ });
2676
+ const selected = await promptSelection('Choose an inbox item', [
2677
+ ...entries.map(({ group, item }) => ({
2678
+ value: item.item_id,
2679
+ label: `[${formatInboxGroupName(group)}] ${item.display_name} — ${formatSmartInboxActionLabel(item)}`,
2680
+ })),
2681
+ { value: '__back__', label: 'Back to main menu' },
2682
+ ]);
2683
+ if (!selected || selected === '__back__') {
2684
+ return;
2685
+ }
2686
+ const match = entries.find(({ item }) => item.item_id === selected);
2687
+ if (!match) {
2688
+ renderInteractiveScreen('Smart Registration Inbox', {
2689
+ subtitle: 'The selected item disappeared during refresh',
2690
+ sections: [{ title: 'Info', lines: [shellListLine('Refreshing inbox list now.')] }],
2691
+ });
2692
+ continue;
2693
+ }
2694
+ if (match.group === 'connected' && (match.item.item_type === 'model' || match.item.item_type === 'runtime')) {
2695
+ await runInteractiveInboxAction(match.item, match.group);
2696
+ continue;
2697
+ }
2698
+ renderInteractiveScreen('Inbox Item', {
2699
+ subtitle: match.item.display_name,
2700
+ sections: buildInteractiveInboxItemSections(match.item, match.group),
2701
+ footerLines: ['Choose what to do with this item below.'],
2702
+ });
2703
+ await runInteractiveInboxAction(match.item, match.group);
2704
+ }
2705
+ };
2706
+ const runInteractiveStart = async () => {
2707
+ while (true) {
2708
+ process.exitCode = 0;
2709
+ await runInteractiveAutoRefresh();
2710
+ const sessionState = await checkBackendSessionState(service);
2711
+ const authenticated = sessionState === 'authorized' || (sessionState === 'unavailable' && Boolean(service.readSessionRef()));
2712
+ if (!authenticated) {
2713
+ await renderInteractiveStatusScreen(sessionState);
2714
+ const selected = await promptSelection('Choose an action', [
2715
+ { value: 'login', label: 'Login / connect this device' },
2716
+ { value: 'create-account', label: 'Create account on Forkit.dev' },
2717
+ { value: 'status', label: 'Status' },
2718
+ { value: 'changes', label: 'View all collected changes' },
2719
+ { value: 'help', label: 'Help' },
2720
+ { value: 'exit', label: 'Exit' },
2721
+ ]);
2722
+ if (!selected || selected === 'exit') {
2723
+ return;
2724
+ }
2725
+ if (selected === 'login') {
2726
+ const approved = await runPublicLogin();
2727
+ if (approved) {
2728
+ continue;
2729
+ }
2730
+ return;
2731
+ }
2732
+ if (selected === 'create-account') {
2733
+ console.log('[forkit-connect] Open Forkit.dev to create or sign into your account:');
2734
+ await openUrlWithFallback(buildCreateAccountUrl(), '[forkit-connect] Open this URL manually:');
2735
+ continue;
2736
+ }
2737
+ if (selected === 'status') {
2738
+ await renderInteractiveStatusScreen(sessionState);
2739
+ await promptSelection('Status', [{ value: 'back', label: 'Back to main menu' }]);
2740
+ continue;
2741
+ }
2742
+ if (selected === 'changes') {
2743
+ renderInteractiveChangesScreen();
2744
+ await promptSelection('Collected changes', [{ value: 'back', label: 'Back to main menu' }]);
2745
+ continue;
2746
+ }
2747
+ showUsage();
2748
+ continue;
2749
+ }
2750
+ const daemonHealth = (0, daemon_1.getDaemonHealth)(service);
2751
+ if (!daemonHealth.running) {
2752
+ await runPublicDaemonStart();
2753
+ process.exitCode = 0;
2754
+ }
2755
+ await renderInteractiveStatusScreen(sessionState);
2756
+ const selected = await promptSelection('Choose an action', [
2757
+ { value: 'status', label: 'Status' },
2758
+ { value: 'changes', label: 'View all collected changes' },
2759
+ { value: 'inbox', label: 'Inbox' },
2760
+ { value: 'register', label: 'Register ready model' },
2761
+ { value: 'start-sync', label: 'Restart background sync' },
2762
+ { value: 'update', label: 'Update' },
2763
+ { value: 'help', label: 'Help' },
2764
+ { value: 'logout', label: 'Log out' },
2765
+ { value: 'exit', label: 'Exit' },
2766
+ ]);
2767
+ if (!selected || selected === 'exit') {
2768
+ return;
2769
+ }
2770
+ if (selected === 'status') {
2771
+ await renderInteractiveStatusScreen(sessionState);
2772
+ await promptSelection('Status', [{ value: 'back', label: 'Back to main menu' }]);
2773
+ continue;
2774
+ }
2775
+ if (selected === 'changes') {
2776
+ renderInteractiveChangesScreen();
2777
+ await promptSelection('Collected changes', [{ value: 'back', label: 'Back to main menu' }]);
2778
+ continue;
2779
+ }
2780
+ if (selected === 'inbox') {
2781
+ await runInteractiveInbox();
2782
+ continue;
2783
+ }
2784
+ if (selected === 'register') {
2785
+ await runInteractiveRegisterMenu();
2786
+ continue;
2787
+ }
2788
+ if (selected === 'start-sync') {
2789
+ await runPublicDaemonStart();
2790
+ continue;
2791
+ }
2792
+ if (selected === 'update') {
2793
+ await runPublicUpdateCheck();
2794
+ continue;
2795
+ }
2796
+ if (selected === 'help') {
2797
+ showUsage();
2798
+ continue;
2799
+ }
2800
+ if (selected === 'logout') {
2801
+ runPublicLogout();
2802
+ }
2803
+ }
2804
+ };
2805
+ const runPublicDaemonStart = async () => {
2806
+ const sessionState = await checkBackendSessionState(service);
2807
+ if (sessionState === 'missing' || sessionState === 'expired') {
2808
+ console.error('Not authenticated. Run forkit-connect login first.');
2809
+ process.exitCode = 2;
2810
+ return;
2811
+ }
2812
+ if (sessionState === 'unavailable') {
2813
+ console.error('Forkit.dev workspace access is unavailable right now. Retry in a moment.');
2814
+ process.exitCode = 2;
2815
+ return;
2816
+ }
2817
+ try {
2818
+ await service.refreshEffectiveBinding();
2819
+ }
2820
+ catch {
2821
+ // Continue with locally stored binding when backend refresh is unavailable.
2822
+ }
2823
+ const operatingMode = resolveOperatingMode(service);
2824
+ const stateBeforeStart = service.getStateStore().readState();
2825
+ const boundWorkspaceId = String(stateBeforeStart.workspace_binding.workspaceId || '').trim();
2826
+ const boundProjectId = String(stateBeforeStart.project_binding.projectId || '').trim();
2827
+ if (operatingMode.mode === 'solo' && (!boundWorkspaceId || !boundProjectId)) {
2828
+ console.log('[forkit-connect] Solo mode active. Personal passport operations remain available without workspace/project scope until you move this device into governed operations.');
2829
+ }
2830
+ if (operatingMode.mode === 'governed' && (!boundWorkspaceId || !boundProjectId)) {
2831
+ console.log('[forkit-connect] Background sync is starting without a selected workspace/project scope.');
2832
+ console.log('[forkit-connect] Choose a workspace only when you register a new model or change scope later.');
2833
+ }
2834
+ const intervalOverride = intervalSecondsArg !== null ? Number(intervalSecondsArg) : undefined;
2835
+ const hasIntervalOverride = typeof intervalOverride === 'number' && Number.isFinite(intervalOverride);
2836
+ const foreground = hasFlag('--foreground');
2837
+ if (foreground) {
2838
+ console.log('Starting daemon in foreground mode. Press Ctrl+C to stop.');
2839
+ if (hasIntervalOverride) {
2840
+ await (0, daemon_1.runDaemonLoopForeground)(service, { intervalSeconds: intervalOverride });
2841
+ }
2842
+ else {
2843
+ await (0, daemon_1.runDaemonLoopForeground)(service);
2844
+ }
2845
+ return;
2846
+ }
2847
+ const launch = await (0, daemon_1.startDaemonBackground)(service, __filename, hasIntervalOverride ? intervalOverride : undefined);
2848
+ if (!launch.pid || !launch.confirmed) {
2849
+ console.error(launch.message || 'Failed to start daemon.');
2850
+ process.exitCode = 2;
2851
+ return;
2852
+ }
2853
+ const daemonStartedAt = new Date().toISOString();
2854
+ console.log(launch.message || `Daemon started in background. pid=${launch.pid}`);
2855
+ console.log(`[forkit-connect] Background sync started at ${daemonStartedAt} — recorded locally.`);
2856
+ console.log('[forkit-connect] Terminal close will not stop background sync. Use forkit-connect stop or log out when you want to disconnect it.');
2857
+ console.log('[forkit-connect] Any model activity detected while sync is running will be captured in evidence logs (forkit-connect changes).');
2858
+ const discovery = await service.runDiscoveryCycle();
2859
+ console.log(`[forkit-connect] Discovery complete. models=${discovery.models.length} runtimes=${discovery.runtimes.length} new=${discovery.newModels} ready_queue=${discovery.queue.queued}`);
2860
+ if (!discovery.ok) {
2861
+ process.exitCode = 2;
2862
+ }
2863
+ };
2864
+ const runPublicDaemonStop = async () => {
2865
+ const stop = await (0, daemon_1.stopDaemonBackground)(service);
2866
+ const daemonStoppedAt = new Date().toISOString();
2867
+ console.log(stop.message);
2868
+ if (stop.stopped) {
2869
+ console.log(`[forkit-connect] Background sync stopped at ${daemonStoppedAt} — recorded locally.`);
2870
+ console.log('[forkit-connect] Any model version changes or activity detected before this point are preserved in evidence logs (forkit-connect changes).');
2871
+ }
2872
+ else {
2873
+ process.exitCode = 2;
2874
+ }
2875
+ };
2876
+ const runPublicSync = async () => {
2877
+ const queue = await service.processQueue();
2878
+ const c2 = await service.flushC2LifecycleEvents({ suppressErrors: true });
2879
+ printJson({
2880
+ drafts: queue,
2881
+ lifecycle: {
2882
+ attempted: c2.attempted,
2883
+ succeeded: c2.succeeded,
2884
+ failed: c2.failed,
2885
+ pending: c2.pending,
2886
+ synced: c2.synced,
2887
+ last_sync_at: c2.lastSyncAt,
2888
+ last_sync_error: c2.lastSyncError,
2889
+ },
2890
+ });
2891
+ };
2892
+ const runWorkspaceList = async () => {
2893
+ const sessionRefValue = String(service.readSessionRef() || '').trim();
2894
+ if (!sessionRefValue) {
2895
+ throw new Error('NOT_AUTHENTICATED');
2896
+ }
2897
+ const api = new api_1.ConnectApiClient({
2898
+ baseUrl: DEFAULT_BASE_URL,
2899
+ sessionRef: sessionRefValue,
2900
+ });
2901
+ const result = await api.getProfileAccess();
2902
+ if (!result.ok || !isProfileAccessResponse(result.body)) {
2903
+ throw new Error(`WORKSPACE_LIST_FAILED:${result.status}`);
2904
+ }
2905
+ const payload = result.body;
2906
+ const rawWorkspaces = Array.isArray(payload.workspaces) ? payload.workspaces : [];
2907
+ const seenWorkspaceIds = new Set();
2908
+ const workspaces = rawWorkspaces.filter((workspace) => {
2909
+ const id = String(workspace.id || '').trim();
2910
+ if (!id)
2911
+ return true;
2912
+ if (seenWorkspaceIds.has(id))
2913
+ return false;
2914
+ seenWorkspaceIds.add(id);
2915
+ return true;
2916
+ });
2917
+ return { payload, workspaces };
2918
+ };
2919
+ const ensureGovernedMode = () => {
2920
+ const operatingMode = resolveOperatingMode(service);
2921
+ if (operatingMode.mode !== 'governed') {
2922
+ console.error('Workspace/project governance is not required on this account. Solo mode is active.');
2923
+ process.exitCode = 2;
2924
+ return null;
2925
+ }
2926
+ return operatingMode;
2927
+ };
2928
+ const buildWorkspaceApi = () => {
2929
+ const sessionRefValue = String(service.readSessionRef() || '').trim();
2930
+ return new api_1.ConnectApiClient({
2931
+ baseUrl: DEFAULT_BASE_URL,
2932
+ sessionRef: sessionRefValue,
2933
+ });
2934
+ };
2935
+ let cachedCliAccountLimits = null;
2936
+ let cachedCliAccountLimitsAt = 0;
2937
+ const loadCliAccountLimits = async (options) => {
2938
+ const displayName = getSessionDisplayName(service.readSessionRef());
2939
+ const now = Date.now();
2940
+ if (!options?.force && cachedCliAccountLimits && now - cachedCliAccountLimitsAt < 15000) {
2941
+ return cachedCliAccountLimits;
2942
+ }
2943
+ const sessionRefValue = String(service.readSessionRef() || '').trim();
2944
+ const fallbackPlanKey = resolveCliPlanKey(service.getServiceEntitlements().tier);
2945
+ const fallbackPlan = CLI_FALLBACK_PLAN_LIMITS[fallbackPlanKey];
2946
+ if (!sessionRefValue) {
2947
+ const fallback = {
2948
+ displayName,
2949
+ planKey: fallbackPlanKey,
2950
+ planName: getCliPlanName(fallbackPlanKey),
2951
+ tier: service.getServiceEntitlements().tier ?? null,
2952
+ draftsUsed: null,
2953
+ draftLimit: fallbackPlan.draftPassports,
2954
+ draftRemaining: null,
2955
+ privatePassportsUsed: null,
2956
+ privatePassportsLimit: fallbackPlan.privatePassports,
2957
+ privatePassportsRemaining: null,
2958
+ workspacesUsed: null,
2959
+ workspaceLimit: fallbackPlan.maxWorkspaces,
2960
+ workspaceRemaining: null,
2961
+ projectsUsed: null,
2962
+ projectLimit: fallbackPlan.maxProjects,
2963
+ projectRemaining: null,
2964
+ runtimeSignalsUsed: null,
2965
+ runtimeSignalsLimit: fallbackPlan.runtimeSignalsPerMonth,
2966
+ runtimeSignalsRemaining: null,
2967
+ };
2968
+ cachedCliAccountLimits = fallback;
2969
+ cachedCliAccountLimitsAt = now;
2970
+ return fallback;
2971
+ }
2972
+ const api = buildWorkspaceApi();
2973
+ const [accessResult, summaryResult, draftsResult, passportsResult] = await Promise.all([
2974
+ api.getProfileAccess().catch(() => null),
2975
+ api.getProductSummary().catch(() => null),
2976
+ api.getPassportDrafts().catch(() => null),
2977
+ api.getPassportsMine(100).catch(() => null),
2978
+ ]);
2979
+ const accessPayload = accessResult?.ok && isProfileAccessResponse(accessResult.body) ? accessResult.body : null;
2980
+ const summaryPayload = summaryResult?.ok && isProductSummaryResponse(summaryResult.body) ? summaryResult.body : null;
2981
+ const workspaces = accessPayload?.workspaces && Array.isArray(accessPayload.workspaces) ? accessPayload.workspaces : [];
2982
+ const drafts = draftsResult?.ok && isPassportDraftListResponse(draftsResult.body) && Array.isArray(draftsResult.body.drafts)
2983
+ ? draftsResult.body.drafts
2984
+ : [];
2985
+ const passports = passportsResult?.ok ? readPassportList(passportsResult.body) : [];
2986
+ const planKey = resolveCliPlanKey(summaryPayload?.planCapabilities?.planKey, summaryPayload?.package?.slug, summaryPayload?.tier, accessPayload?.summary?.tier, service.getServiceEntitlements().tier);
2987
+ const fallback = CLI_FALLBACK_PLAN_LIMITS[planKey];
2988
+ let projectsUsed = typeof summaryPayload?.usage?.projects === 'number' ? summaryPayload.usage.projects : null;
2989
+ if (projectsUsed === null && workspaces.length > 0) {
2990
+ try {
2991
+ const projectResults = await Promise.all(workspaces.map(async (workspace) => {
2992
+ const workspaceId = String(workspace.id || '').trim();
2993
+ if (!workspaceId)
2994
+ return 0;
2995
+ const result = await api.getWorkspaceProjects(workspaceId);
2996
+ if (!result.ok || !isWorkspaceProjectsResponse(result.body))
2997
+ return 0;
2998
+ return Array.isArray(result.body.projects) ? result.body.projects.length : 0;
2999
+ }));
3000
+ projectsUsed = projectResults.reduce((total, count) => total + count, 0);
3001
+ }
3002
+ catch {
3003
+ projectsUsed = null;
3004
+ }
3005
+ }
3006
+ const privatePassportsUsed = typeof summaryPayload?.usage?.privatePassports === 'number'
3007
+ ? summaryPayload.usage.privatePassports
3008
+ : passports.some((passport) => Object.prototype.hasOwnProperty.call(passport, 'visibility'))
3009
+ ? passports.filter((passport) => String(passport.visibility || '').trim().toLowerCase() === 'private').length
3010
+ : null;
3011
+ const draftsUsed = typeof summaryPayload?.usage?.drafts === 'number' ? summaryPayload.usage.drafts : drafts.length;
3012
+ const workspacesUsed = workspaces.length;
3013
+ const draftLimit = summaryPayload?.planCapabilities?.registration?.draftPassports
3014
+ ?? summaryPayload?.entitlements?.draftPassports
3015
+ ?? fallback.draftPassports;
3016
+ const privatePassportsLimit = summaryPayload?.planCapabilities?.registration?.privatePassports
3017
+ ?? summaryPayload?.entitlements?.privatePassports
3018
+ ?? fallback.privatePassports;
3019
+ const workspaceLimit = summaryPayload?.planCapabilities?.governance?.maxGovernedWorkspaces
3020
+ ?? summaryPayload?.entitlements?.maxWorkspaces
3021
+ ?? fallback.maxWorkspaces;
3022
+ const projectLimit = summaryPayload?.planCapabilities?.governance?.maxGovernedProjects
3023
+ ?? summaryPayload?.entitlements?.maxProjects
3024
+ ?? fallback.maxProjects;
3025
+ const runtimeSignalsLimit = summaryPayload?.planCapabilities?.runtimeSignals?.runtimeSignalsPerMonth
3026
+ ?? fallback.runtimeSignalsPerMonth;
3027
+ const runtimeSignalsUsed = typeof summaryPayload?.usage?.runtimeSignals === 'number'
3028
+ ? summaryPayload.usage.runtimeSignals
3029
+ : null;
3030
+ const resolved = {
3031
+ displayName,
3032
+ planKey,
3033
+ planName: getCliPlanName(planKey),
3034
+ tier: String(summaryPayload?.tier || accessPayload?.summary?.tier || service.getServiceEntitlements().tier || '').trim() || null,
3035
+ draftsUsed,
3036
+ draftLimit,
3037
+ draftRemaining: remainingFromLimit(draftLimit, draftsUsed),
3038
+ privatePassportsUsed,
3039
+ privatePassportsLimit,
3040
+ privatePassportsRemaining: remainingFromLimit(privatePassportsLimit, privatePassportsUsed),
3041
+ workspacesUsed,
3042
+ workspaceLimit,
3043
+ workspaceRemaining: remainingFromLimit(workspaceLimit, workspacesUsed),
3044
+ projectsUsed,
3045
+ projectLimit,
3046
+ projectRemaining: remainingFromLimit(projectLimit, projectsUsed),
3047
+ runtimeSignalsUsed,
3048
+ runtimeSignalsLimit,
3049
+ runtimeSignalsRemaining: remainingFromLimit(runtimeSignalsLimit, runtimeSignalsUsed),
3050
+ };
3051
+ cachedCliAccountLimits = resolved;
3052
+ cachedCliAccountLimitsAt = now;
3053
+ return resolved;
3054
+ };
3055
+ const createProjectInWorkspace = async (api, selectedWorkspaceId, options) => {
3056
+ let nextProjectName = projectNameArg;
3057
+ let nextProjectDescription = projectDescriptionArg;
3058
+ if (!nextProjectName && options?.interactivePrompt !== false) {
3059
+ nextProjectName = await promptText('Project name: ');
3060
+ if (!nextProjectName) {
3061
+ console.error('Project creation cancelled.');
3062
+ process.exitCode = 2;
3063
+ return null;
3064
+ }
3065
+ nextProjectDescription = await promptText('Project description (optional): ', { allowEmpty: true });
3066
+ }
3067
+ if (!nextProjectName) {
3068
+ console.error('Missing project name. Use --project-name or run interactively.');
3069
+ process.exitCode = 2;
3070
+ return null;
3071
+ }
3072
+ const createResult = await api.createWorkspaceProject(selectedWorkspaceId, {
3073
+ name: nextProjectName,
3074
+ ...(nextProjectDescription ? { description: nextProjectDescription } : {}),
3075
+ });
3076
+ if (!createResult.ok || !isWorkspaceProjectCreateResponse(createResult.body)) {
3077
+ console.error(`Project creation failed (${createResult.status}).`);
3078
+ if (createResult.body) {
3079
+ console.error(typeof createResult.body === 'string' ? createResult.body : JSON.stringify(createResult.body));
3080
+ }
3081
+ process.exitCode = 2;
3082
+ return null;
3083
+ }
3084
+ await loadCliAccountLimits({ force: true }).catch(() => null);
3085
+ return createResult.body.project ?? null;
3086
+ };
3087
+ const runWorkspaceCreate = async () => {
3088
+ const operatingMode = ensureGovernedMode();
3089
+ if (!operatingMode)
3090
+ return;
3091
+ const workspaceName = getArg('--name') || await promptText('Workspace name: ');
3092
+ if (!workspaceName) {
3093
+ console.error('Workspace creation cancelled.');
3094
+ process.exitCode = 2;
3095
+ return;
3096
+ }
3097
+ const workspaceDescription = descriptionArg ?? await promptText('Workspace description (optional): ', { allowEmpty: true });
3098
+ const api = buildWorkspaceApi();
3099
+ const createWorkspaceResult = await api.createWorkspace({
3100
+ name: workspaceName,
3101
+ ...(workspaceDescription ? { description: workspaceDescription } : {}),
3102
+ });
3103
+ if (!createWorkspaceResult.ok || !isWorkspaceCreateResponse(createWorkspaceResult.body)) {
3104
+ console.error(`Workspace creation failed (${createWorkspaceResult.status}).`);
3105
+ if (createWorkspaceResult.body) {
3106
+ console.error(typeof createWorkspaceResult.body === 'string' ? createWorkspaceResult.body : JSON.stringify(createWorkspaceResult.body));
3107
+ }
3108
+ process.exitCode = 2;
3109
+ return;
3110
+ }
3111
+ const workspace = createWorkspaceResult.body.workspace && typeof createWorkspaceResult.body.workspace === 'object'
3112
+ ? createWorkspaceResult.body.workspace
3113
+ : null;
3114
+ const createdWorkspaceId = String(workspace?.id || '').trim();
3115
+ if (!createdWorkspaceId) {
3116
+ console.error('Workspace creation returned no workspace id.');
3117
+ process.exitCode = 2;
3118
+ return;
3119
+ }
3120
+ await loadCliAccountLimits({ force: true }).catch(() => null);
3121
+ let shouldCreateFirstProject = Boolean(projectNameArg);
3122
+ if (!shouldCreateFirstProject) {
3123
+ const selected = await promptSelection('Create first project now?', [
3124
+ { value: 'create', label: 'Create project now' },
3125
+ { value: 'skip', label: 'Skip for now' },
3126
+ ]);
3127
+ shouldCreateFirstProject = selected === 'create';
3128
+ }
3129
+ let createdProject = null;
3130
+ if (shouldCreateFirstProject) {
3131
+ createdProject = await createProjectInWorkspace(api, createdWorkspaceId, { interactivePrompt: true });
3132
+ if (process.exitCode && process.exitCode !== 0) {
3133
+ return;
3134
+ }
3135
+ }
3136
+ let bindingResult = null;
3137
+ if (createdProject?.id) {
3138
+ bindingResult = await service.bindWorkspaceProject(createdWorkspaceId, String(createdProject.id));
3139
+ }
3140
+ printJson({
3141
+ operating_mode: operatingMode.mode,
3142
+ tier: operatingMode.tier,
3143
+ workspace,
3144
+ project: createdProject,
3145
+ binding: bindingResult,
3146
+ });
3147
+ };
3148
+ const runWorkspaceStatus = async () => {
3149
+ const payload = await buildWorkspaceStatusPayload();
3150
+ if (hasFlag('--json')) {
3151
+ printJson(payload);
3152
+ return;
3153
+ }
3154
+ console.log('[forkit-connect] Workspace status');
3155
+ console.log(`- mode=${payload.operating_mode}`);
3156
+ console.log(`- tier=${payload.tier || 'unknown'}`);
3157
+ console.log(`- workspace=${payload.workspace_id || 'not selected'}`);
3158
+ console.log(`- project=${payload.project_id || 'not selected'}`);
3159
+ console.log(`- session=${payload.session_state}`);
3160
+ console.log(`- binding_state=${payload.binding_state}`);
3161
+ console.log(`- daemon=${payload.daemon_status}`);
3162
+ console.log(`- ready_to_connect=${payload.ready_to_connect_count}`);
3163
+ console.log(`- needs_confirmation=${payload.needs_confirmation_count}`);
3164
+ console.log(`- connected=${payload.connected_count}`);
3165
+ if (payload.lifecycle_note) {
3166
+ console.log(`- note=${payload.lifecycle_note}`);
3167
+ }
3168
+ };
3169
+ const runWorkspaceSelect = async () => {
3170
+ const operatingMode = ensureGovernedMode();
3171
+ if (!operatingMode)
3172
+ return;
3173
+ const { workspaces } = await runWorkspaceList();
3174
+ if (workspaces.length === 0) {
3175
+ console.log('No accessible workspaces returned by Forkit.dev.');
3176
+ await runWorkspaceCreate();
3177
+ return;
3178
+ }
3179
+ let selectedWorkspaceId = workspaceId;
3180
+ if (!selectedWorkspaceId) {
3181
+ selectedWorkspaceId = await promptSelection('Accessible workspaces', [
3182
+ ...workspaces.map((workspace) => ({
3183
+ value: String(workspace.id || ''),
3184
+ label: `${workspace.name || 'Unnamed workspace'} (${String(workspace.role || 'unknown')})`,
3185
+ })),
3186
+ { value: '__create_workspace__', label: 'Create new workspace' },
3187
+ ]);
3188
+ if (!selectedWorkspaceId) {
3189
+ console.error('Workspace selection cancelled.');
3190
+ process.exitCode = 2;
3191
+ return;
3192
+ }
3193
+ }
3194
+ if (selectedWorkspaceId === '__create_workspace__') {
3195
+ await runWorkspaceCreate();
3196
+ return;
3197
+ }
3198
+ const api = buildWorkspaceApi();
3199
+ const projectsResult = await api.getWorkspaceProjects(selectedWorkspaceId);
3200
+ if (!projectsResult.ok || !isWorkspaceProjectsResponse(projectsResult.body)) {
3201
+ console.error(`Project listing failed (${projectsResult.status}).`);
3202
+ process.exitCode = 2;
3203
+ return;
3204
+ }
3205
+ const projects = Array.isArray(projectsResult.body.projects) ? projectsResult.body.projects : [];
3206
+ if (projects.length === 0) {
3207
+ console.log('No projects found in this workspace.');
3208
+ const createdProject = await createProjectInWorkspace(api, selectedWorkspaceId, { interactivePrompt: true });
3209
+ if (!createdProject?.id) {
3210
+ return;
3211
+ }
3212
+ const bindingResult = await service.bindWorkspaceProject(selectedWorkspaceId, String(createdProject.id));
3213
+ printJson({
3214
+ operating_mode: operatingMode.mode,
3215
+ tier: operatingMode.tier,
3216
+ created_project: createdProject,
3217
+ binding: bindingResult,
3218
+ });
3219
+ return;
3220
+ }
3221
+ let selectedProjectValue = projectId;
3222
+ if (!selectedProjectValue) {
3223
+ selectedProjectValue = await promptSelection('Projects in selected workspace', [
3224
+ ...projects.map((project) => ({
3225
+ value: String(project.id || ''),
3226
+ label: project.name || 'Unnamed project',
3227
+ })),
3228
+ { value: '__create_project__', label: 'Create new project' },
3229
+ ]);
3230
+ if (!selectedProjectValue) {
3231
+ console.error('Project selection cancelled.');
3232
+ process.exitCode = 2;
3233
+ return;
3234
+ }
3235
+ }
3236
+ if (selectedProjectValue === '__create_project__') {
3237
+ const createdProject = await createProjectInWorkspace(api, selectedWorkspaceId, { interactivePrompt: true });
3238
+ if (!createdProject?.id) {
3239
+ return;
3240
+ }
3241
+ const bindingResult = await service.bindWorkspaceProject(selectedWorkspaceId, String(createdProject.id));
3242
+ printJson({
3243
+ operating_mode: operatingMode.mode,
3244
+ tier: operatingMode.tier,
3245
+ created_project: createdProject,
3246
+ binding: bindingResult,
3247
+ });
3248
+ return;
3249
+ }
3250
+ const bindingResult = await service.bindWorkspaceProject(selectedWorkspaceId, selectedProjectValue);
3251
+ printJson(bindingResult);
3252
+ };
3253
+ const runPublicRegister = async () => {
3254
+ const state = service.getStateStore().readState();
3255
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
3256
+ if (!storedSessionRef) {
3257
+ console.error('Not authenticated. Run forkit-connect login first.');
3258
+ process.exitCode = 2;
3259
+ return;
3260
+ }
3261
+ try {
3262
+ await service.refreshEffectiveBinding();
3263
+ }
3264
+ catch {
3265
+ // Use local binding when backend refresh is temporarily unavailable.
3266
+ }
3267
+ const operatingMode = resolveOperatingMode(service);
3268
+ let currentState = service.getStateStore().readState();
3269
+ let boundWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim();
3270
+ let boundProjectId = String(currentState.project_binding.projectId || '').trim();
3271
+ if (operatingMode.mode === 'governed' && (!boundWorkspaceId || !boundProjectId)) {
3272
+ console.log('[forkit-connect] Governed mode requires a workspace/project selection before registration.');
3273
+ await runWorkspaceSelect();
3274
+ if (process.exitCode && process.exitCode !== 0) {
3275
+ return;
3276
+ }
3277
+ currentState = service.getStateStore().readState();
3278
+ boundWorkspaceId = String(currentState.workspace_binding.workspaceId || '').trim();
3279
+ boundProjectId = String(currentState.project_binding.projectId || '').trim();
3280
+ if (!boundWorkspaceId || !boundProjectId) {
3281
+ console.error('No workspace/project selected. Run forkit-connect workspace select first.');
3282
+ process.exitCode = 2;
3283
+ return;
3284
+ }
3285
+ }
3286
+ const normalizeRegisterErrorMessage = (raw) => {
3287
+ if (raw === 'MODEL_NOT_FOUND')
3288
+ return 'Model not found in local detected models. Run forkit-connect scan first.';
3289
+ if (raw === 'MODEL_SELECTION_AMBIGUOUS')
3290
+ return 'Model selection is ambiguous. Use a full discoveryHash.';
3291
+ if (raw === 'DRAFT_ALREADY_EXISTS')
3292
+ return 'A backend draft already exists for this model. No duplicate draft created.';
3293
+ if (raw === 'SIMILAR_PASSPORT_EXISTS')
3294
+ return 'A similar or existing passport already exists in your Forkit.dev account. Review existing records before creating a new draft.';
3295
+ if (raw === 'WORKSPACE_PROJECT_BINDING_REQUIRED')
3296
+ return 'No governed workspace/project scope is linked yet. Select or create scope first.';
3297
+ if (raw === 'DRAFT_CREATION_NOT_ALLOWED_BY_BINDING')
3298
+ return 'Draft creation is not active for this binding yet. Complete Connect approval or update consent on Forkit.dev first.';
3299
+ return raw;
3300
+ };
3301
+ const runRegisterOne = async (targetModelName) => {
3302
+ try {
3303
+ const result = await service.connectDetectedModel(targetModelName);
3304
+ return {
3305
+ ok: true,
3306
+ model: result.model.model,
3307
+ draftId: result.draftId ?? null,
3308
+ gaid: result.gaid ?? null,
3309
+ message: result.action,
3310
+ };
3311
+ }
3312
+ catch (error) {
3313
+ const rawMessage = error instanceof Error ? error.message : 'register_failed';
3314
+ return {
3315
+ ok: false,
3316
+ model: targetModelName,
3317
+ message: normalizeRegisterErrorMessage(rawMessage),
3318
+ };
3319
+ }
3320
+ };
3321
+ if (hasFlag('--all-ready')) {
3322
+ const inbox = service.buildSmartRegistrationInbox();
3323
+ const readyModels = inbox.groups.ready_to_connect
3324
+ .filter((item) => item.item_type === 'model' && item.recommended_action === 'create_passport_draft')
3325
+ .map((item) => item.display_name);
3326
+ if (readyModels.length === 0) {
3327
+ console.log('No ready local models need registration.');
3328
+ return;
3329
+ }
3330
+ const results = [];
3331
+ for (const item of readyModels) {
3332
+ results.push(await runRegisterOne(item));
3333
+ }
3334
+ printJson({
3335
+ attempted: readyModels.length,
3336
+ results,
3337
+ });
3338
+ if (results.some((item) => !item.ok)) {
3339
+ process.exitCode = 2;
3340
+ }
3341
+ return;
3342
+ }
3343
+ if (!modelName) {
3344
+ const inbox = service.buildSmartRegistrationInbox();
3345
+ const readyModels = inbox.groups.ready_to_connect
3346
+ .filter((item) => item.item_type === 'model')
3347
+ .map((item) => item.display_name);
3348
+ printJson({
3349
+ operating_mode: operatingMode.mode,
3350
+ tier: operatingMode.tier,
3351
+ workspace_id: boundWorkspaceId,
3352
+ project_id: boundProjectId,
3353
+ ready_models: readyModels,
3354
+ next: readyModels.length
3355
+ ? 'Run forkit-connect register --model "<name>" or forkit-connect register --all-ready'
3356
+ : 'Run forkit-connect scan first or review forkit-connect inbox',
3357
+ });
3358
+ return;
3359
+ }
3360
+ const result = await runRegisterOne(modelName);
3361
+ printJson(result);
3362
+ if (!result.ok) {
3363
+ process.exitCode = 2;
3364
+ }
3365
+ };
3366
+ const runPublicIgnore = () => {
3367
+ const positionalName = args[1] && !args[1].startsWith('-') ? args[1] : null;
3368
+ const requestedName = positionalName || getArg('--ignore-model') || modelName;
3369
+ const requestedDigest = getArg('--digest') || getArg('--ignore-digest');
3370
+ if (!requestedName) {
3371
+ console.error('Missing model name. Use forkit-connect ignore <modelName> [--digest <sha>].');
3372
+ process.exitCode = 2;
3373
+ return;
3374
+ }
3375
+ const resolvedModel = resolveSingleDetectedModelByName(service, requestedName, requestedDigest);
3376
+ if (!resolvedModel) {
3377
+ console.error('Model not found or ambiguous. Provide --digest when more than one detected model shares the same name.');
3378
+ process.exitCode = 2;
3379
+ return;
3380
+ }
3381
+ const ignored = service.markModelIgnored(resolvedModel.model, resolvedModel.digest);
3382
+ if (!ignored) {
3383
+ console.error('Ignore failed.');
3384
+ process.exitCode = 2;
3385
+ return;
3386
+ }
3387
+ printJson({
3388
+ ignored: true,
3389
+ model: resolvedModel.model,
3390
+ digest: resolvedModel.digest,
3391
+ runtime: resolvedModel.runtime,
3392
+ });
3393
+ };
3394
+ if (command === 'init') {
3395
+ runPublicConnectInit();
3396
+ return;
3397
+ }
3398
+ if (command === 'status') {
3399
+ await runPublicConnectStatus();
3400
+ return;
3401
+ }
3402
+ if (command === 'changes') {
3403
+ runPublicCollectedChanges();
3404
+ return;
3405
+ }
3406
+ if (command === 'stop') {
3407
+ await runPublicDaemonStop();
3408
+ return;
3409
+ }
3410
+ if (command === 'inbox') {
3411
+ runPublicConnectInbox();
3412
+ return;
3413
+ }
3414
+ if (command === 'start') {
3415
+ if (hasFlag('--foreground')) {
3416
+ await runPublicDaemonStart();
3417
+ return;
3418
+ }
3419
+ await runInteractiveStart();
3420
+ return;
3421
+ }
3422
+ if (command === 'sync') {
3423
+ await runPublicSync();
3424
+ return;
3425
+ }
3426
+ if (command === 'workspace') {
3427
+ const subcommand = args[1] || 'status';
3428
+ try {
3429
+ if (subcommand === 'list') {
3430
+ const { workspaces } = await runWorkspaceList();
3431
+ if (hasFlag('--json')) {
3432
+ printJson({ workspaces });
3433
+ return;
3434
+ }
3435
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
3436
+ console.log(`[forkit-connect] Workspaces: ${workspaces.length}`);
3437
+ if (accountLimits) {
3438
+ console.log(`- welcome=${accountLimits.displayName || 'there'}`);
3439
+ console.log(`- plan=${accountLimits.planName}`);
3440
+ console.log(`- workspaces_left=${formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')}`);
3441
+ console.log(`- projects_left=${formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')}`);
3442
+ }
3443
+ for (const workspace of workspaces) {
3444
+ console.log(formatWorkspaceAccessLine(workspace));
3445
+ }
3446
+ return;
3447
+ }
3448
+ if (subcommand === 'select') {
3449
+ await runWorkspaceSelect();
3450
+ return;
3451
+ }
3452
+ if (subcommand === 'create') {
3453
+ await runWorkspaceCreate();
3454
+ return;
3455
+ }
3456
+ if (subcommand === 'status') {
3457
+ await runWorkspaceStatus();
3458
+ return;
3459
+ }
3460
+ }
3461
+ catch (error) {
3462
+ const message = error instanceof Error ? error.message : 'workspace_command_failed';
3463
+ if (message === 'NOT_AUTHENTICATED') {
3464
+ console.error('Not authenticated. Run forkit-connect login first.');
3465
+ }
3466
+ else if (message.startsWith('WORKSPACE_LIST_FAILED:')) {
3467
+ console.error(`Workspace listing failed (${message.split(':')[1]}).`);
3468
+ }
3469
+ else {
3470
+ console.error(message);
3471
+ }
3472
+ process.exitCode = 2;
3473
+ return;
3474
+ }
3475
+ console.error('Usage: forkit-connect workspace <list|select|create|status>');
3476
+ process.exitCode = 2;
3477
+ return;
3478
+ }
3479
+ if (command === 'ignore') {
3480
+ runPublicIgnore();
3481
+ return;
3482
+ }
3483
+ if (command === 'review') {
3484
+ const reviewScan = await service.scanRuntime();
3485
+ const snapshot = service.buildReviewSnapshot(reviewScan.summary);
3486
+ if (snapshot.total === 0) {
3487
+ console.log('No review items found. Run `forkit-connect scan` first or ensure local runtimes are available.');
3488
+ return;
3489
+ }
3490
+ printReviewSnapshot(snapshot);
3491
+ return;
3492
+ }
3493
+ if (command === 'connect') {
3494
+ const subcommand = args[1];
3495
+ if (subcommand === 'start') {
3496
+ runPublicConnectInit();
3497
+ return;
3498
+ }
3499
+ if (subcommand === 'init') {
3500
+ runPublicConnectInit();
3501
+ return;
3502
+ }
3503
+ if (subcommand === 'status') {
3504
+ await runPublicConnectStatus();
3505
+ return;
3506
+ }
3507
+ if (subcommand === 'inbox') {
3508
+ runPublicConnectInbox();
3509
+ return;
3510
+ }
3511
+ if (subcommand === 'services') {
3512
+ printJson(service.getConnectServices());
3513
+ return;
3514
+ }
3515
+ if (subcommand === 'permissions') {
3516
+ printJson(service.getConnectPermissions());
3517
+ return;
3518
+ }
3519
+ if (subcommand === 'handoff') {
3520
+ printJson(service.buildConnectHandoff());
3521
+ return;
3522
+ }
3523
+ if (subcommand === 'evolution') {
3524
+ if (args[2] !== 'review') {
3525
+ console.error('Usage: forkit-connect connect evolution review');
3526
+ process.exitCode = 2;
3527
+ return;
3528
+ }
3529
+ printJson(service.reviewModelEvolution());
3530
+ return;
3531
+ }
3532
+ if (subcommand === 'runtime') {
3533
+ if (args[2] === 'review') {
3534
+ printRuntimeReview(service.getRuntimePassportReview());
3535
+ return;
3536
+ }
3537
+ if (args[2] === 'status') {
3538
+ printRuntimeStatus(service.getRuntimePassportStatus());
3539
+ return;
3540
+ }
3541
+ console.error('Usage: forkit-connect connect runtime <review|status>');
3542
+ process.exitCode = 2;
3543
+ return;
3544
+ }
3545
+ const selector = subcommand;
3546
+ if (!selector) {
3547
+ console.error('Missing required argument: <modelNameOrDiscoveryHash>');
3548
+ process.exitCode = 2;
3549
+ return;
3550
+ }
3551
+ try {
3552
+ const result = await service.connectDetectedModel(selector);
3553
+ if (result.action === 'already_bound') {
3554
+ console.log('Model is already bound to a Passport.');
3555
+ if (result.gaid) {
3556
+ console.log(`Passport GAID: ${result.gaid}`);
3557
+ }
3558
+ return;
3559
+ }
3560
+ if (result.action === 'already_pending') {
3561
+ console.log('Model already has a pending Passport draft.');
3562
+ if (result.draftId) {
3563
+ console.log(`Draft ID: ${result.draftId}`);
3564
+ }
3565
+ return;
3566
+ }
3567
+ if (result.action === 'draft_created') {
3568
+ console.log('Passport draft created from detected model.');
3569
+ console.log(`Model: ${result.model.model}`);
3570
+ if (result.draftId) {
3571
+ console.log(`Draft ID: ${result.draftId}`);
3572
+ }
3573
+ if (result.gaid) {
3574
+ console.log(`Passport GAID: ${result.gaid}`);
3575
+ }
3576
+ return;
3577
+ }
3578
+ console.log('Passport draft queued locally from detected model.');
3579
+ console.log(`Model: ${result.model.model}`);
3580
+ console.log('Complete login later and run scan or queue processing to sync it.');
3581
+ return;
3582
+ }
3583
+ catch (error) {
3584
+ const message = error instanceof Error ? error.message : 'connect_failed';
3585
+ if (message === 'MODEL_NOT_FOUND') {
3586
+ console.error('Model not found in local detected models. Run forkit-connect scan first.');
3587
+ process.exitCode = 2;
3588
+ return;
3589
+ }
3590
+ if (message === 'MODEL_SELECTION_AMBIGUOUS') {
3591
+ console.error('Model selection is ambiguous. Use the full discoveryHash.');
3592
+ process.exitCode = 2;
3593
+ return;
3594
+ }
3595
+ if (message === 'WORKSPACE_PROJECT_BINDING_REQUIRED') {
3596
+ console.error('No governed workspace/project scope is linked yet. Personal mode stays available, but this action needs governed scope. After sign-in, run `forkit-connect workspaces`, `forkit-connect projects --workspace <id>`, and `forkit-connect bind --workspace <id> --project <id>` first.');
3597
+ process.exitCode = 2;
3598
+ return;
3599
+ }
3600
+ if (message === 'DRAFT_CREATION_NOT_ALLOWED_BY_BINDING') {
3601
+ console.error('Draft creation is not active for this binding yet. Complete Connect approval or update consent on Forkit.dev before using governed scope from this device.');
3602
+ process.exitCode = 2;
3603
+ return;
3604
+ }
3605
+ if (message === 'DRAFT_ALREADY_EXISTS') {
3606
+ console.error('A backend draft already exists for this model. No duplicate draft created.');
3607
+ process.exitCode = 2;
3608
+ return;
3609
+ }
3610
+ if (message === 'SIMILAR_PASSPORT_EXISTS') {
3611
+ console.error('A similar or existing passport already exists in your Forkit.dev account. Review inbox or existing passports before creating a new draft.');
3612
+ process.exitCode = 2;
3613
+ return;
3614
+ }
3615
+ if (message.startsWith('DRAFT_CREATE_FAILED:')) {
3616
+ console.error(`[forkit-connect] Draft creation failed (${message.split(':')[1]}).`);
3617
+ process.exitCode = 2;
3618
+ return;
3619
+ }
3620
+ console.error(message);
3621
+ process.exitCode = 2;
3622
+ return;
3623
+ }
3624
+ }
3625
+ if (command === 'c2') {
3626
+ const subcommand = args[1];
3627
+ if (subcommand === 'status') {
3628
+ printC2Status(service.getC2Status());
3629
+ return;
3630
+ }
3631
+ if (subcommand === 'set-key') {
3632
+ const gaid = getArg('--heartbeat-gaid');
3633
+ const apiKey = getArg('--heartbeat-key');
3634
+ if (!gaid || !apiKey) {
3635
+ console.error('[forkit-connect] --heartbeat-gaid and --heartbeat-key are required for c2 set-key');
3636
+ process.exitCode = 1;
3637
+ return;
3638
+ }
3639
+ const result = service.configureRuntimeSignalKey({ gaid, apiKey });
3640
+ if (!result.stored) {
3641
+ console.error('[forkit-connect] Failed to store runtime signal key. Verify the gaid matches a bound model and the key is 8–512 characters.');
3642
+ process.exitCode = 2;
3643
+ return;
3644
+ }
3645
+ console.log(JSON.stringify({
3646
+ stored: result.stored,
3647
+ gaid: result.gaid,
3648
+ key_ref: result.keyRef,
3649
+ backfilled_events: result.backfilled,
3650
+ }, null, 2));
3651
+ return;
3652
+ }
3653
+ if (subcommand === 'sync') {
3654
+ const syncResult = await service.flushC2LifecycleEvents({
3655
+ runtimeSignalApiKey: getArg('--heartbeat-key'),
3656
+ gaid: getArg('--heartbeat-gaid'),
3657
+ suppressErrors: true,
3658
+ });
3659
+ console.log(JSON.stringify({
3660
+ attempted: syncResult.attempted,
3661
+ succeeded: syncResult.succeeded,
3662
+ failed: syncResult.failed,
3663
+ pending: syncResult.pending,
3664
+ synced: syncResult.synced,
3665
+ last_sync_at: syncResult.lastSyncAt,
3666
+ last_sync_error: syncResult.lastSyncError,
3667
+ }, null, 2));
3668
+ return;
3669
+ }
3670
+ usage();
3671
+ process.exitCode = 1;
3672
+ return;
3673
+ }
3674
+ if (command === 'train') {
3675
+ const subcommand = args[1];
3676
+ const buildSessionId = args[2];
3677
+ if (subcommand === 'init') {
3678
+ const trainName = getArg('--name');
3679
+ const framework = getArg('--framework');
3680
+ const task = getArg('--task');
3681
+ const datasetRef = getArg('--dataset-ref');
3682
+ if (!trainName || !framework || !task) {
3683
+ console.error('Missing required arguments: --name <modelName> --framework <framework> --task <task>');
3684
+ process.exitCode = 2;
3685
+ return;
3686
+ }
3687
+ try {
3688
+ const result = await service.initTrainingBuildSession({
3689
+ modelName: trainName,
3690
+ framework,
3691
+ task,
3692
+ datasetRef,
3693
+ });
3694
+ console.log(JSON.stringify({
3695
+ build_session_id: result.buildSession.build_session_id,
3696
+ model_name: result.buildSession.model_name,
3697
+ framework: result.buildSession.framework,
3698
+ task: result.buildSession.task,
3699
+ draft_action: result.draftAction,
3700
+ draft_id: result.draftId,
3701
+ passport_gaid: result.gaid,
3702
+ status: result.buildSession.status,
3703
+ }, null, 2));
3704
+ return;
3705
+ }
3706
+ catch (error) {
3707
+ const message = error instanceof Error ? error.message : 'train_init_failed';
3708
+ if (message === 'DRAFT_ALREADY_EXISTS') {
3709
+ console.error('A backend draft already exists for this training session metadata.');
3710
+ }
3711
+ else if (message.startsWith('DRAFT_CREATE_FAILED:')) {
3712
+ console.error(`[forkit-connect] Training draft creation failed (${message.split(':')[1]}).`);
3713
+ }
3714
+ else {
3715
+ console.error(message);
3716
+ }
3717
+ process.exitCode = 2;
3718
+ return;
3719
+ }
3720
+ }
3721
+ if (subcommand === 'event') {
3722
+ const eventType = getArg('--type');
3723
+ if (!buildSessionId || !eventType) {
3724
+ console.error('Missing required arguments: train event <build_session_id> --type <event_type>');
3725
+ process.exitCode = 2;
3726
+ return;
3727
+ }
3728
+ if (!TRAIN_EVENT_TYPES.includes(eventType)) {
3729
+ console.error(`Unsupported event type. Use one of: ${TRAIN_EVENT_TYPES.join(', ')}`);
3730
+ process.exitCode = 2;
3731
+ return;
3732
+ }
3733
+ try {
3734
+ const session = service.recordTrainingLifecycleEvent(buildSessionId, eventType);
3735
+ console.log(JSON.stringify({
3736
+ build_session_id: session.build_session_id,
3737
+ status: session.status,
3738
+ latest_lifecycle_event: session.lifecycle_events.at(-1)?.event_type ?? null,
3739
+ }, null, 2));
3740
+ return;
3741
+ }
3742
+ catch (error) {
3743
+ console.error(error instanceof Error ? error.message : 'train_event_failed');
3744
+ process.exitCode = 2;
3745
+ return;
3746
+ }
3747
+ }
3748
+ if (subcommand === 'dataset') {
3749
+ const datasetRef = getArg('--ref');
3750
+ const changeType = getArg('--change');
3751
+ if (!buildSessionId || !datasetRef || !changeType) {
3752
+ console.error('Missing required arguments: train dataset <build_session_id> --ref <dataset_ref> --change <change_type>');
3753
+ process.exitCode = 2;
3754
+ return;
3755
+ }
3756
+ if (!TRAIN_DATASET_CHANGE_TYPES.includes(changeType)) {
3757
+ console.error(`Unsupported dataset change type. Use one of: ${TRAIN_DATASET_CHANGE_TYPES.join(', ')}`);
3758
+ process.exitCode = 2;
3759
+ return;
3760
+ }
3761
+ try {
3762
+ const session = service.recordTrainingDatasetChange(buildSessionId, datasetRef, changeType);
3763
+ console.log(JSON.stringify({
3764
+ build_session_id: session.build_session_id,
3765
+ dataset_refs_count: session.dataset_refs.length,
3766
+ latest_dataset_change: session.dataset_refs.at(-1) ?? null,
3767
+ }, null, 2));
3768
+ return;
3769
+ }
3770
+ catch (error) {
3771
+ console.error(error instanceof Error ? error.message : 'train_dataset_failed');
3772
+ process.exitCode = 2;
3773
+ return;
3774
+ }
3775
+ }
3776
+ if (subcommand === 'metric') {
3777
+ const metricName = getArg('--name');
3778
+ const metricValue = getArg('--value');
3779
+ if (!buildSessionId || !metricName || metricValue === null) {
3780
+ console.error('Missing required arguments: train metric <build_session_id> --name <metric_name> --value <metric_value>');
3781
+ process.exitCode = 2;
3782
+ return;
3783
+ }
3784
+ const numericValue = Number(metricValue);
3785
+ if (!Number.isFinite(numericValue)) {
3786
+ console.error('Invalid --value. Use a finite number.');
3787
+ process.exitCode = 2;
3788
+ return;
3789
+ }
3790
+ try {
3791
+ const session = service.recordTrainingMetric(buildSessionId, metricName, numericValue);
3792
+ console.log(JSON.stringify({
3793
+ build_session_id: session.build_session_id,
3794
+ metrics_count: session.metrics.length,
3795
+ latest_metric: session.metrics.at(-1) ?? null,
3796
+ }, null, 2));
3797
+ return;
3798
+ }
3799
+ catch (error) {
3800
+ console.error(error instanceof Error ? error.message : 'train_metric_failed');
3801
+ process.exitCode = 2;
3802
+ return;
3803
+ }
3804
+ }
3805
+ if (subcommand === 'version') {
3806
+ const versionName = getArg('--version');
3807
+ const reason = getArg('--reason');
3808
+ if (!buildSessionId || !versionName || !reason) {
3809
+ console.error('Missing required arguments: train version <build_session_id> --version <version_name> --reason <reason>');
3810
+ process.exitCode = 2;
3811
+ return;
3812
+ }
3813
+ try {
3814
+ const session = service.recordTrainingVersion(buildSessionId, versionName, reason);
3815
+ console.log(JSON.stringify({
3816
+ build_session_id: session.build_session_id,
3817
+ versions_count: session.versions.length,
3818
+ latest_version: session.versions.at(-1) ?? null,
3819
+ }, null, 2));
3820
+ return;
3821
+ }
3822
+ catch (error) {
3823
+ console.error(error instanceof Error ? error.message : 'train_version_failed');
3824
+ process.exitCode = 2;
3825
+ return;
3826
+ }
3827
+ }
3828
+ if (subcommand === 'artifact') {
3829
+ const artifactPath = getArg('--path');
3830
+ if (!buildSessionId || !artifactPath) {
3831
+ console.error('Missing required arguments: train artifact <build_session_id> --path <artifact_path> [--hash-artifact]');
3832
+ process.exitCode = 2;
3833
+ return;
3834
+ }
3835
+ try {
3836
+ const session = service.recordTrainingArtifact(buildSessionId, artifactPath, hasFlag('--hash-artifact'));
3837
+ console.log(JSON.stringify({
3838
+ build_session_id: session.build_session_id,
3839
+ artifact_refs_count: session.artifact_refs.length,
3840
+ latest_artifact: session.artifact_refs.at(-1) ?? null,
3841
+ }, null, 2));
3842
+ return;
3843
+ }
3844
+ catch (error) {
3845
+ console.error(error instanceof Error ? error.message : 'train_artifact_failed');
3846
+ process.exitCode = 2;
3847
+ return;
3848
+ }
3849
+ }
3850
+ if (subcommand === 'status') {
3851
+ try {
3852
+ printTrainStatus(service.getTrainingStatus(buildSessionId));
3853
+ return;
3854
+ }
3855
+ catch (error) {
3856
+ console.error(error instanceof Error ? error.message : 'train_status_failed');
3857
+ process.exitCode = 2;
3858
+ return;
3859
+ }
3860
+ }
3861
+ usage();
3862
+ process.exitCode = 1;
3863
+ return;
3864
+ }
3865
+ if (command === 'agent') {
3866
+ const subcommand = args[1];
3867
+ if (subcommand === 'scan') {
3868
+ const result = await service.scanAgents();
3869
+ console.log(`[forkit-connect] Agents detected=${result.agents.length} new=${result.newAgents} known=${result.knownAgents} inactive_marked=${result.inactiveMarked}`);
3870
+ for (const agent of result.agents) {
3871
+ console.log(`- ${agent.agent_name} | type=${agent.agent_type} | status=${agent.status} | id=${shortId(agent.agent_id)} | last_seen_at=${agent.last_seen_at}`);
3872
+ }
3873
+ return;
3874
+ }
3875
+ if (subcommand === 'review') {
3876
+ const snapshot = service.buildAgentReviewSnapshot();
3877
+ if (snapshot.total === 0) {
3878
+ console.log('No agent items found. Run `forkit-connect agent scan` first.');
3879
+ return;
3880
+ }
3881
+ printAgentReview(snapshot);
3882
+ return;
3883
+ }
3884
+ if (subcommand === 'connect') {
3885
+ const selector = args[2];
3886
+ if (!selector) {
3887
+ console.error('Missing required argument: <agentNameOrId>');
3888
+ process.exitCode = 2;
3889
+ return;
3890
+ }
3891
+ try {
3892
+ const agent = service.connectAgent(selector);
3893
+ console.log(JSON.stringify({
3894
+ agent_id: agent.agent_id,
3895
+ agent_name: agent.agent_name,
3896
+ agent_type: agent.agent_type,
3897
+ status: agent.status,
3898
+ }, null, 2));
3899
+ return;
3900
+ }
3901
+ catch (error) {
3902
+ const message = error instanceof Error ? error.message : 'agent_connect_failed';
3903
+ console.error(message === 'AGENT_NOT_FOUND' ? 'Agent not found. Run forkit-connect agent scan first.' : message === 'AGENT_SELECTION_AMBIGUOUS' ? 'Agent selection is ambiguous. Use the full agent_id.' : message);
3904
+ process.exitCode = 2;
3905
+ return;
3906
+ }
3907
+ }
3908
+ if (subcommand === 'link') {
3909
+ const selector = args[2];
3910
+ if (!selector || !modelName) {
3911
+ console.error('Missing required arguments: agent link <agentNameOrId> --model <modelNameOrDiscoveryHash>');
3912
+ process.exitCode = 2;
3913
+ return;
3914
+ }
3915
+ try {
3916
+ const link = service.linkAgentToModel(selector, modelName);
3917
+ console.log(JSON.stringify(link, null, 2));
3918
+ return;
3919
+ }
3920
+ catch (error) {
3921
+ const message = error instanceof Error ? error.message : 'agent_link_failed';
3922
+ if (message === 'AGENT_NOT_FOUND') {
3923
+ console.error('Agent not found. Run forkit-connect agent scan first.');
3924
+ }
3925
+ else if (message === 'MODEL_NOT_FOUND') {
3926
+ console.error('Model not found in local detected models. Run forkit-connect scan first.');
3927
+ }
3928
+ else if (message === 'MODEL_SELECTION_AMBIGUOUS') {
3929
+ console.error('Model selection is ambiguous. Use the full discoveryHash.');
3930
+ }
3931
+ else if (message === 'AGENT_SELECTION_AMBIGUOUS') {
3932
+ console.error('Agent selection is ambiguous. Use the full agent_id.');
3933
+ }
3934
+ else {
3935
+ console.error(message);
3936
+ }
3937
+ process.exitCode = 2;
3938
+ return;
3939
+ }
3940
+ }
3941
+ if (subcommand === 'status') {
3942
+ printAgentStatus(service.getAgentStatus());
3943
+ return;
3944
+ }
3945
+ if (subcommand === 'ledger') {
3946
+ const selector = args[2];
3947
+ try {
3948
+ printAgentLedger(service.getAgentModelUsageLedger(selector));
3949
+ return;
3950
+ }
3951
+ catch (error) {
3952
+ const message = error instanceof Error ? error.message : 'agent_ledger_failed';
3953
+ if (message === 'AGENT_NOT_FOUND') {
3954
+ console.error('Agent not found. Run forkit-connect agent scan first.');
3955
+ }
3956
+ else if (message === 'AGENT_SELECTION_AMBIGUOUS') {
3957
+ console.error('Agent selection is ambiguous. Use the full agent_id.');
3958
+ }
3959
+ else {
3960
+ console.error(message);
3961
+ }
3962
+ process.exitCode = 2;
3963
+ return;
3964
+ }
3965
+ }
3966
+ usage();
3967
+ process.exitCode = 1;
3968
+ return;
3969
+ }
3970
+ if (command === 'tray') {
3971
+ const subcommand = args[1];
3972
+ if (subcommand === 'status') {
3973
+ const trayStatus = service.getTrayStatus();
3974
+ const menu = service.buildTrayMenuModel();
3975
+ printTrayStatus(trayStatus, menu);
3976
+ return;
3977
+ }
3978
+ usage();
3979
+ process.exitCode = 1;
3980
+ return;
3981
+ }
3982
+ if (command === 'notify') {
3983
+ const subcommand = args[1];
3984
+ if (subcommand === 'once') {
3985
+ console.log(JSON.stringify(service.deliverPendingNotifications(), null, 2));
3986
+ return;
3987
+ }
3988
+ if (subcommand === 'status') {
3989
+ console.log(JSON.stringify(service.getNotificationStatus(), null, 2));
3990
+ return;
3991
+ }
3992
+ if (subcommand === 'test') {
3993
+ console.log(JSON.stringify(service.sendTestNotification(), null, 2));
3994
+ return;
3995
+ }
3996
+ usage();
3997
+ process.exitCode = 1;
3998
+ return;
3999
+ }
4000
+ if (command === 'config') {
4001
+ const subcommand = args[1];
4002
+ const key = args[2];
4003
+ const value = args[3];
4004
+ if (subcommand === 'get') {
4005
+ printConfig(service.getConfig());
4006
+ return;
4007
+ }
4008
+ if (subcommand === 'set') {
4009
+ if (key === 'discovery_mode') {
4010
+ if (value !== 'advisory_only' && value !== 'auto_draft') {
4011
+ console.error('Invalid discovery_mode. Use advisory_only or auto_draft.');
4012
+ process.exitCode = 2;
4013
+ return;
4014
+ }
4015
+ printConfig(service.setDiscoveryMode(value));
4016
+ return;
4017
+ }
4018
+ if (key === 'daemon_interval_seconds') {
4019
+ const interval = Number(value);
4020
+ if (!Number.isFinite(interval)) {
4021
+ console.error('Invalid daemon_interval_seconds. Use a number of seconds.');
4022
+ process.exitCode = 2;
4023
+ return;
4024
+ }
4025
+ printConfig(service.setDaemonIntervalSeconds(interval));
4026
+ return;
4027
+ }
4028
+ if (key === 'notifications_enabled') {
4029
+ if (value !== 'true' && value !== 'false') {
4030
+ console.error('Invalid notifications_enabled. Use true or false.');
4031
+ process.exitCode = 2;
4032
+ return;
4033
+ }
4034
+ printConfig(service.setNotificationsEnabled(value === 'true'));
4035
+ return;
4036
+ }
4037
+ if (key === 'notification_min_interval_seconds') {
4038
+ const interval = Number(value);
4039
+ if (!Number.isFinite(interval)) {
4040
+ console.error('Invalid notification_min_interval_seconds. Use a number of seconds.');
4041
+ process.exitCode = 2;
4042
+ return;
4043
+ }
4044
+ printConfig(service.setNotificationMinIntervalSeconds(interval));
4045
+ return;
4046
+ }
4047
+ if (key === 'evolution_bridge_enabled') {
4048
+ if (value !== 'true' && value !== 'false') {
4049
+ console.error('Invalid evolution_bridge_enabled. Use true or false.');
4050
+ process.exitCode = 2;
4051
+ return;
4052
+ }
4053
+ printConfig(service.setEvolutionBridgeEnabled(value === 'true'));
4054
+ return;
4055
+ }
4056
+ console.error('Supported config keys: discovery_mode, daemon_interval_seconds, notifications_enabled, notification_min_interval_seconds, evolution_bridge_enabled');
4057
+ process.exitCode = 2;
4058
+ return;
4059
+ }
4060
+ usage();
4061
+ process.exitCode = 1;
4062
+ return;
4063
+ }
4064
+ if (command === 'daemon') {
4065
+ const subcommand = args[1];
4066
+ const intervalOverride = intervalSecondsArg !== null ? Number(intervalSecondsArg) : undefined;
4067
+ const hasIntervalOverride = typeof intervalOverride === 'number' && Number.isFinite(intervalOverride);
4068
+ if (subcommand === 'status') {
4069
+ const daemonStatus = service.getDaemonStatus();
4070
+ console.log(JSON.stringify({
4071
+ ...daemonStatus,
4072
+ config: service.getConfig(),
4073
+ }, null, 2));
4074
+ if (daemonStatus.last_scan_summary) {
4075
+ printDiscoverySummary(daemonStatus.last_scan_summary);
4076
+ }
4077
+ return;
4078
+ }
4079
+ if (subcommand === 'health') {
4080
+ console.log(JSON.stringify({
4081
+ ...(0, daemon_1.getDaemonHealth)(service),
4082
+ config: service.getConfig(),
4083
+ }, null, 2));
4084
+ return;
4085
+ }
4086
+ if (subcommand === 'stop') {
4087
+ const stop = await (0, daemon_1.stopDaemonBackground)(service);
4088
+ console.log(stop.message);
4089
+ if (!stop.stopped) {
4090
+ process.exitCode = 2;
4091
+ }
4092
+ return;
4093
+ }
4094
+ if (subcommand === 'run-loop') {
4095
+ if (hasIntervalOverride) {
4096
+ await (0, daemon_1.runDaemonLoopForeground)(service, { intervalSeconds: intervalOverride });
4097
+ }
4098
+ else {
4099
+ await (0, daemon_1.runDaemonLoopForeground)(service);
4100
+ }
4101
+ return;
4102
+ }
4103
+ if (subcommand === 'restart') {
4104
+ const restart = await (0, daemon_1.restartDaemonBackground)(service, __filename, hasIntervalOverride ? intervalOverride : undefined);
4105
+ if (!restart.confirmed) {
4106
+ console.error(restart.message || 'Failed to restart daemon.');
4107
+ process.exitCode = 2;
4108
+ return;
4109
+ }
4110
+ console.log(`Daemon restarted in background. pid=${restart.pid}`);
4111
+ return;
4112
+ }
4113
+ if (subcommand === 'start') {
4114
+ await runPublicDaemonStart();
4115
+ return;
4116
+ }
4117
+ usage();
4118
+ process.exitCode = 1;
4119
+ return;
4120
+ }
4121
+ if (command === 'pulse') {
4122
+ const subcommand = args[1];
4123
+ const historyLimit = limitArg !== null ? Number(limitArg) : 20;
4124
+ if (subcommand === 'once') {
4125
+ const summary = await service.runVitalityPulseCycle();
4126
+ printPulseSummary(summary);
4127
+ return;
4128
+ }
4129
+ if (subcommand === 'status') {
4130
+ const state = service.getStateStore().readState();
4131
+ console.log(JSON.stringify({
4132
+ last_pulse_at: state.last_pulse_at,
4133
+ last_pulse_error: state.last_pulse_error,
4134
+ }, null, 2));
4135
+ if (state.last_pulse_summary) {
4136
+ printPulseSummary(state.last_pulse_summary);
4137
+ }
4138
+ else {
4139
+ console.log('No pulse summary recorded yet. Run `forkit-connect pulse once` first.');
4140
+ }
4141
+ return;
4142
+ }
4143
+ if (subcommand === 'history') {
4144
+ if (!Number.isFinite(historyLimit) || historyLimit <= 0) {
4145
+ console.error('Invalid --limit value. Use a positive number.');
4146
+ process.exitCode = 2;
4147
+ return;
4148
+ }
4149
+ const events = service.getPulseHistory(historyLimit);
4150
+ if (events.length === 0) {
4151
+ console.log('No pulse history recorded yet. Run `forkit-connect pulse once` first.');
4152
+ return;
4153
+ }
4154
+ printPulseHistory(events, Math.floor(historyLimit));
4155
+ return;
4156
+ }
4157
+ usage();
4158
+ process.exitCode = 1;
4159
+ return;
4160
+ }
4161
+ if (command === 'update-check') {
4162
+ await runPublicUpdateCheck();
4163
+ return;
4164
+ }
4165
+ if (command === 'bound') {
4166
+ const state = service.getStateStore().readState();
4167
+ const bindings = Array.isArray(state.model_bindings)
4168
+ ? state.model_bindings.filter((binding) => binding.status !== 'ignored')
4169
+ : [];
4170
+ console.log(`Bound models: ${bindings.length}`);
4171
+ if (bindings.length === 0) {
4172
+ console.log('No model passports are bound yet.');
4173
+ return;
4174
+ }
4175
+ const sessionRef = String(service.readSessionRef() || '').trim();
4176
+ const api = sessionRef
4177
+ ? new api_1.ConnectApiClient({
4178
+ baseUrl: DEFAULT_BASE_URL,
4179
+ sessionRef,
4180
+ })
4181
+ : null;
4182
+ for (const binding of bindings) {
4183
+ const detectedModel = state.detected_models.find((item) => `${item.model}#${item.digest}` === binding.modelKey);
4184
+ const modelName = detectedModel?.model || (0, heartbeat_1.readBoundModelName)(binding.modelKey);
4185
+ const gaid = String(binding.gaid || 'n/a');
4186
+ const localDraftId = String(binding.draftId || 'n/a');
4187
+ const localWorkspaceId = String(state.workspace_binding.workspaceId || 'n/a');
4188
+ const localProjectId = String(state.project_binding.projectId || 'n/a');
4189
+ const runtime = detectedModel?.runtime || 'n/a';
4190
+ const source = detectedModel?.sourceLabel || 'n/a';
4191
+ console.log(`- ${modelName}`);
4192
+ console.log(` GAID: ${gaid}`);
4193
+ console.log(` Draft ID: ${localDraftId}`);
4194
+ console.log(` Workspace: ${localWorkspaceId}`);
4195
+ console.log(` Project: ${localProjectId}`);
4196
+ console.log(` Runtime: ${runtime}`);
4197
+ console.log(` Source: ${source}`);
4198
+ console.log(` Status: ${binding.status}`);
4199
+ if (api && binding.gaid) {
4200
+ const verifyResult = await api.getPassport(binding.gaid);
4201
+ if (verifyResult.ok && isPassportResponse(verifyResult.body) && verifyResult.body.passport) {
4202
+ console.log(' Backend: verified');
4203
+ }
4204
+ else if (verifyResult.status === 404) {
4205
+ console.log(' Backend: missing (404)');
4206
+ }
4207
+ else if (verifyResult.status === 403) {
4208
+ console.log(' Backend: inaccessible (403)');
4209
+ }
4210
+ else if (verifyResult.status === 400) {
4211
+ console.log(' Backend: invalid GAID (400)');
4212
+ }
4213
+ else {
4214
+ console.log(` Backend: lookup failed (${verifyResult.status})`);
4215
+ }
4216
+ }
4217
+ }
4218
+ return;
4219
+ }
4220
+ if (command === 'heartbeat') {
4221
+ const updateCheck = await (0, update_1.checkForUpdates)();
4222
+ if (updateCheck.ok && updateCheck.availability === 'required-update') {
4223
+ printUpdateCheckLines((0, update_1.formatUpdateCheckLines)(updateCheck), true);
4224
+ process.exitCode = 2;
4225
+ return;
4226
+ }
4227
+ const heartbeatResult = await (0, heartbeat_1.sendBoundHeartbeat)(service, { baseUrl: DEFAULT_BASE_URL });
4228
+ if (!heartbeatResult.ok) {
4229
+ console.error(heartbeatResult.message);
4230
+ if (heartbeatResult.code === 'heartbeat_failed' && heartbeatResult.details) {
4231
+ console.error(typeof heartbeatResult.details === 'string' ? heartbeatResult.details : JSON.stringify(heartbeatResult.details));
4232
+ }
4233
+ process.exitCode = 2;
4234
+ return;
4235
+ }
4236
+ console.log('Runtime heartbeat sent.');
4237
+ console.log(`Passport GAID: ${heartbeatResult.gaid}`);
4238
+ console.log(`Session ID: ${heartbeatResult.sessionId}`);
4239
+ if (heartbeatResult.lastCheckinAt) {
4240
+ console.log(`Last Check-in: ${heartbeatResult.lastCheckinAt}`);
4241
+ }
4242
+ if (updateCheck.ok && updateCheck.availability === 'update-available') {
4243
+ printUpdateCheckLines((0, update_1.formatUpdateCheckLines)(updateCheck));
4244
+ }
4245
+ return;
4246
+ }
4247
+ if (command === 'workspaces') {
4248
+ const state = service.getStateStore().readState();
4249
+ const sessionRef = String(service.readSessionRef() || '').trim();
4250
+ if (!sessionRef) {
4251
+ console.error('Not authenticated. Run forkit-connect login first.');
4252
+ process.exitCode = 2;
4253
+ return;
4254
+ }
4255
+ const api = new api_1.ConnectApiClient({
4256
+ baseUrl: DEFAULT_BASE_URL,
4257
+ sessionRef,
4258
+ });
4259
+ const result = await api.getProfileAccess();
4260
+ if (!result.ok || !isProfileAccessResponse(result.body)) {
4261
+ console.error(`[forkit-connect] Workspace listing failed (${result.status}).`);
4262
+ if (result.body) {
4263
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
4264
+ }
4265
+ process.exitCode = 2;
4266
+ return;
4267
+ }
4268
+ const payload = result.body;
4269
+ const tokenPayload = parseJwtPayload(sessionRef);
4270
+ const email = typeof tokenPayload?.email === 'string' ? tokenPayload.email : null;
4271
+ const platformRole = typeof payload.summary?.platformRole === 'string'
4272
+ ? payload.summary.platformRole
4273
+ : typeof tokenPayload?.role === 'string'
4274
+ ? tokenPayload.role
4275
+ : null;
4276
+ if (email || platformRole) {
4277
+ const identityParts = [
4278
+ email ? `account=${email}` : null,
4279
+ platformRole ? `platform_role=${platformRole}` : null,
4280
+ ].filter(Boolean);
4281
+ console.log(`[forkit-connect] ${identityParts.join(' | ')}`);
4282
+ }
4283
+ const workspaces = Array.isArray(payload.workspaces) ? payload.workspaces : [];
4284
+ console.log(`[forkit-connect] Workspaces: ${workspaces.length}`);
4285
+ if (workspaces.length === 0) {
4286
+ console.log('[forkit-connect] No accessible workspaces returned by /api/profiles/access.');
4287
+ return;
4288
+ }
4289
+ for (const workspace of workspaces) {
4290
+ console.log(formatWorkspaceAccessLine(workspace));
4291
+ }
4292
+ return;
4293
+ }
4294
+ if (command === 'projects') {
4295
+ const state = service.getStateStore().readState();
4296
+ const sessionRef = String(service.readSessionRef() || '').trim();
4297
+ if (!sessionRef) {
4298
+ console.error('Not authenticated. Run forkit-connect login first.');
4299
+ process.exitCode = 2;
4300
+ return;
4301
+ }
4302
+ if (!workspaceId) {
4303
+ console.error('Missing required argument: --workspace <workspaceId>');
4304
+ process.exitCode = 2;
4305
+ return;
4306
+ }
4307
+ const api = new api_1.ConnectApiClient({
4308
+ baseUrl: DEFAULT_BASE_URL,
4309
+ sessionRef,
4310
+ });
4311
+ const result = await api.getWorkspaceProjects(workspaceId);
4312
+ if (!result.ok) {
4313
+ if (result.status === 403 || result.status === 404 || result.status === 400) {
4314
+ console.error('Workspace not found or not accessible.');
4315
+ process.exitCode = 2;
4316
+ return;
4317
+ }
4318
+ console.error(`[forkit-connect] Project listing failed (${result.status}).`);
4319
+ if (result.body) {
4320
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
4321
+ }
4322
+ process.exitCode = 2;
4323
+ return;
4324
+ }
4325
+ if (!isWorkspaceProjectsResponse(result.body)) {
4326
+ console.error(`[forkit-connect] Project listing failed (${result.status}).`);
4327
+ process.exitCode = 2;
4328
+ return;
4329
+ }
4330
+ console.log('Authenticated successfully.');
4331
+ const projects = Array.isArray(result.body.projects) ? result.body.projects : [];
4332
+ console.log(`Projects: ${projects.length}`);
4333
+ if (projects.length === 0) {
4334
+ console.log('No projects found in this workspace.');
4335
+ console.log('Run `forkit-connect workspace select --workspace <id> --project-name "<name>"` to create the first project here.');
4336
+ return;
4337
+ }
4338
+ for (const project of projects) {
4339
+ console.log(formatWorkspaceProjectLine(project));
4340
+ }
4341
+ return;
4342
+ }
4343
+ if (command === 'bind') {
4344
+ const state = service.getStateStore().readState();
4345
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
4346
+ if (!storedSessionRef) {
4347
+ console.error('Not authenticated. Run forkit-connect login first.');
4348
+ process.exitCode = 2;
4349
+ return;
4350
+ }
4351
+ if (!workspaceId) {
4352
+ console.error('Missing required argument: --workspace <workspaceId>');
4353
+ process.exitCode = 2;
4354
+ return;
4355
+ }
4356
+ if (!projectId) {
4357
+ console.error('Missing required argument: --project <projectId>');
4358
+ process.exitCode = 2;
4359
+ return;
4360
+ }
4361
+ const api = new api_1.ConnectApiClient({
4362
+ baseUrl: DEFAULT_BASE_URL,
4363
+ sessionRef: storedSessionRef,
4364
+ });
4365
+ const accessResult = await api.getProfileAccess();
4366
+ if (!accessResult.ok || !isProfileAccessResponse(accessResult.body)) {
4367
+ console.error(`[forkit-connect] Workspace validation failed (${accessResult.status}).`);
4368
+ if (accessResult.body) {
4369
+ console.error(typeof accessResult.body === 'string' ? accessResult.body : JSON.stringify(accessResult.body));
4370
+ }
4371
+ process.exitCode = 2;
4372
+ return;
4373
+ }
4374
+ const workspaces = Array.isArray(accessResult.body.workspaces) ? accessResult.body.workspaces : [];
4375
+ const isAccessibleWorkspace = workspaces.some((workspace) => String(workspace.id || '').trim() === workspaceId);
4376
+ if (!isAccessibleWorkspace) {
4377
+ console.error('Workspace not found or not accessible.');
4378
+ process.exitCode = 2;
4379
+ return;
4380
+ }
4381
+ const projectsResult = await api.getWorkspaceProjects(workspaceId);
4382
+ if (!projectsResult.ok) {
4383
+ if (projectsResult.status === 400 || projectsResult.status === 403 || projectsResult.status === 404) {
4384
+ console.error('Workspace not found or not accessible.');
4385
+ process.exitCode = 2;
4386
+ return;
4387
+ }
4388
+ console.error(`[forkit-connect] Project validation failed (${projectsResult.status}).`);
4389
+ if (projectsResult.body) {
4390
+ console.error(typeof projectsResult.body === 'string' ? projectsResult.body : JSON.stringify(projectsResult.body));
4391
+ }
4392
+ process.exitCode = 2;
4393
+ return;
4394
+ }
4395
+ if (!isWorkspaceProjectsResponse(projectsResult.body)) {
4396
+ console.error(`[forkit-connect] Project validation failed (${projectsResult.status}).`);
4397
+ process.exitCode = 2;
4398
+ return;
4399
+ }
4400
+ const projects = Array.isArray(projectsResult.body.projects) ? projectsResult.body.projects : [];
4401
+ const belongsToWorkspace = projects.some((project) => String(project.id || '').trim() === projectId);
4402
+ if (!belongsToWorkspace) {
4403
+ console.error('Project not found in workspace.');
4404
+ process.exitCode = 2;
4405
+ return;
4406
+ }
4407
+ const bindingResult = await service.bindWorkspaceProject(workspaceId, projectId);
4408
+ console.log(bindingResult.message);
4409
+ console.log(`Workspace: ${bindingResult.workspaceId ?? workspaceId}`);
4410
+ console.log(`Project: ${bindingResult.projectId ?? projectId}`);
4411
+ return;
4412
+ }
4413
+ if (command === 'register') {
4414
+ if (hasFlag('--all-ready') || !getArg('--model')) {
4415
+ await runPublicRegister();
4416
+ return;
4417
+ }
4418
+ const state = service.getStateStore().readState();
4419
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
4420
+ if (!storedSessionRef) {
4421
+ console.error('Not authenticated. Run forkit-connect login first.');
4422
+ process.exitCode = 2;
4423
+ return;
4424
+ }
4425
+ const boundWorkspaceId = String(state.workspace_binding.workspaceId || '').trim();
4426
+ const boundProjectId = String(state.project_binding.projectId || '').trim();
4427
+ if (!boundWorkspaceId || !boundProjectId) {
4428
+ console.error('No workspace/project bound. Run forkit-connect bind first.');
4429
+ process.exitCode = 2;
4430
+ return;
4431
+ }
4432
+ if (!modelName) {
4433
+ console.error('Missing required argument: --model <modelName>');
4434
+ process.exitCode = 2;
4435
+ return;
4436
+ }
4437
+ const detectedModels = Array.isArray(state.detected_models) ? state.detected_models : [];
4438
+ const model = detectedModels.find((item) => item.model === modelName && item.status !== 'ignored');
4439
+ if (!model) {
4440
+ console.error('Model not found in local detected models. Run forkit-connect scan first.');
4441
+ process.exitCode = 2;
4442
+ return;
4443
+ }
4444
+ const modelKey = `${model.model}#${model.digest}`;
4445
+ const existingBinding = (state.model_bindings || []).find((binding) => binding.modelKey === modelKey && binding.status !== 'ignored');
4446
+ if (existingBinding?.draftId) {
4447
+ console.error('Model already registered. No duplicate draft created.');
4448
+ process.exitCode = 2;
4449
+ return;
4450
+ }
4451
+ const payload = {
4452
+ passportType: 'model',
4453
+ name: model.model,
4454
+ metadata: {
4455
+ passport_type: 'model',
4456
+ runtime: model.runtime,
4457
+ provider: model.runtime,
4458
+ detection_source: model.sourceLabel,
4459
+ workspaceId: boundWorkspaceId,
4460
+ projectId: boundProjectId,
4461
+ workspace_id: boundWorkspaceId,
4462
+ project_id: boundProjectId,
4463
+ digest: model.digest,
4464
+ sizeBytes: model.sizeBytes,
4465
+ modifiedAt: model.modifiedAt,
4466
+ detectedAt: model.detectedAt,
4467
+ connection_status: 'polling_ready',
4468
+ },
4469
+ originChannel: 'cli',
4470
+ originTool: 'forkit-connect',
4471
+ workspaceId: boundWorkspaceId,
4472
+ projectId: boundProjectId,
4473
+ connectionStatus: 'polling_ready',
4474
+ };
4475
+ const api = new api_1.ConnectApiClient({
4476
+ baseUrl: DEFAULT_BASE_URL,
4477
+ sessionRef: storedSessionRef,
4478
+ });
4479
+ const result = await api.createPassportDraft(payload);
4480
+ if (!result.ok) {
4481
+ if (result.status === 409) {
4482
+ console.error('Model already registered in backend. No duplicate draft created.');
4483
+ process.exitCode = 2;
4484
+ return;
4485
+ }
4486
+ console.error(`[forkit-connect] Model registration failed (${result.status}).`);
4487
+ if (result.body) {
4488
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
4489
+ }
4490
+ process.exitCode = 2;
4491
+ return;
4492
+ }
4493
+ const responseBody = result.body && typeof result.body === 'object'
4494
+ ? result.body
4495
+ : null;
4496
+ const draft = responseBody?.draft && typeof responseBody.draft === 'object'
4497
+ ? responseBody.draft
4498
+ : null;
4499
+ const passport = responseBody?.passport && typeof responseBody.passport === 'object'
4500
+ ? responseBody.passport
4501
+ : null;
4502
+ const draftId = typeof draft?.id === 'string'
4503
+ ? draft.id
4504
+ : typeof responseBody?.id === 'string'
4505
+ ? responseBody.id
4506
+ : null;
4507
+ const gaid = typeof passport?.gaid === 'string'
4508
+ ? passport.gaid
4509
+ : typeof draft?.gaid === 'string'
4510
+ ? draft.gaid
4511
+ : null;
4512
+ service.getStateStore().upsertModelBinding({
4513
+ modelKey,
4514
+ discoveryHash: model.discoveryHash,
4515
+ registrationKey: (0, discovery_1.buildModelRegistrationKey)(model, state.runtime_identity.runtimeId),
4516
+ gaid,
4517
+ draftId,
4518
+ status: 'pending',
4519
+ updatedAt: new Date().toISOString(),
4520
+ });
4521
+ service.getStateStore().addEvidenceEvent({
4522
+ type: 'model_draft_created',
4523
+ details: {
4524
+ model: model.model,
4525
+ digest: model.digest,
4526
+ workspaceId: boundWorkspaceId,
4527
+ projectId: boundProjectId,
4528
+ draftId,
4529
+ gaid,
4530
+ backendStatus: result.status,
4531
+ },
4532
+ });
4533
+ console.log('Model registration draft created.');
4534
+ if (draftId) {
4535
+ console.log(`Draft ID: ${draftId}`);
4536
+ }
4537
+ if (gaid) {
4538
+ console.log(`Passport GAID: ${gaid}`);
4539
+ }
4540
+ return;
4541
+ }
4542
+ if (command === 'drafts') {
4543
+ const state = service.getStateStore().readState();
4544
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
4545
+ if (!storedSessionRef) {
4546
+ console.error('Not authenticated. Run forkit-connect login first.');
4547
+ process.exitCode = 2;
4548
+ return;
4549
+ }
4550
+ const api = new api_1.ConnectApiClient({
4551
+ baseUrl: DEFAULT_BASE_URL,
4552
+ sessionRef: storedSessionRef,
4553
+ });
4554
+ const result = await api.getPassportDrafts();
4555
+ if (!result.ok || !isPassportDraftListResponse(result.body)) {
4556
+ console.error(`[forkit-connect] Draft listing failed (${result.status}).`);
4557
+ if (result.body) {
4558
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
4559
+ }
4560
+ process.exitCode = 2;
4561
+ return;
4562
+ }
4563
+ const drafts = Array.isArray(result.body.drafts) ? result.body.drafts : [];
4564
+ console.log(`Drafts: ${drafts.length}`);
4565
+ if (drafts.length === 0) {
4566
+ console.log('No drafts found.');
4567
+ return;
4568
+ }
4569
+ for (const draft of drafts) {
4570
+ console.log(formatDraftLine(draft));
4571
+ }
4572
+ return;
4573
+ }
4574
+ if (command === 'publish') {
4575
+ const state = service.getStateStore().readState();
4576
+ const storedSessionRef = String(service.readSessionRef() || '').trim();
4577
+ if (!storedSessionRef) {
4578
+ console.error('Not authenticated. Run forkit-connect login first.');
4579
+ process.exitCode = 2;
4580
+ return;
4581
+ }
4582
+ if (!draftId) {
4583
+ console.error('Missing required argument: --draft <draftId>');
4584
+ process.exitCode = 2;
4585
+ return;
4586
+ }
4587
+ const api = new api_1.ConnectApiClient({
4588
+ baseUrl: DEFAULT_BASE_URL,
4589
+ sessionRef: storedSessionRef,
4590
+ });
4591
+ const lookupResult = await api.getPassportDraft(draftId);
4592
+ if (!lookupResult.ok) {
4593
+ if (lookupResult.status === 403 || lookupResult.status === 404) {
4594
+ console.error('Draft not found or not accessible.');
4595
+ process.exitCode = 2;
4596
+ return;
4597
+ }
4598
+ console.error(`[forkit-connect] Draft lookup failed (${lookupResult.status}).`);
4599
+ if (lookupResult.body) {
4600
+ console.error(typeof lookupResult.body === 'string' ? lookupResult.body : JSON.stringify(lookupResult.body));
4601
+ }
4602
+ process.exitCode = 2;
4603
+ return;
4604
+ }
4605
+ if (!isPassportDraftResponse(lookupResult.body) || !lookupResult.body.draft) {
4606
+ console.error('Draft not found or not accessible.');
4607
+ process.exitCode = 2;
4608
+ return;
4609
+ }
4610
+ const publishResult = await api.publishPassportDraft(draftId);
4611
+ if (!publishResult.ok) {
4612
+ if (publishResult.status === 403 || publishResult.status === 404) {
4613
+ console.error('Draft not found or not accessible.');
4614
+ process.exitCode = 2;
4615
+ return;
4616
+ }
4617
+ if (publishResult.status === 409) {
4618
+ const body = publishResult.body && typeof publishResult.body === 'object'
4619
+ ? publishResult.body
4620
+ : null;
4621
+ const passport = body?.passport && typeof body.passport === 'object'
4622
+ ? body.passport
4623
+ : null;
4624
+ const gaid = typeof passport?.gaid === 'string' ? passport.gaid : null;
4625
+ console.error(gaid ? `Draft already published: ${gaid}` : 'Draft already published.');
4626
+ process.exitCode = 2;
4627
+ return;
4628
+ }
4629
+ const body = publishResult.body && typeof publishResult.body === 'object'
4630
+ ? publishResult.body
4631
+ : null;
4632
+ const message = typeof body?.error === 'string' ? body.error : null;
4633
+ console.error(message || `[forkit-connect] Draft publish failed (${publishResult.status}).`);
4634
+ if (publishResult.body) {
4635
+ console.error(typeof publishResult.body === 'string' ? publishResult.body : JSON.stringify(publishResult.body));
4636
+ }
4637
+ process.exitCode = 2;
4638
+ return;
4639
+ }
4640
+ if (!isPassportDraftPublishResponse(publishResult.body) || !publishResult.body.passport) {
4641
+ console.error(`[forkit-connect] Draft publish failed (${publishResult.status}).`);
4642
+ process.exitCode = 2;
4643
+ return;
4644
+ }
4645
+ const passport = publishResult.body.passport;
4646
+ const publishedGaid = typeof passport.gaid === 'string' ? passport.gaid : null;
4647
+ const bindingState = service.getStateStore().readState();
4648
+ const matchingBinding = bindingState.model_bindings.find((binding) => binding.draftId === draftId);
4649
+ if (matchingBinding) {
4650
+ service.getStateStore().upsertModelBinding({
4651
+ ...matchingBinding,
4652
+ gaid: publishedGaid,
4653
+ status: 'bound',
4654
+ updatedAt: new Date().toISOString(),
4655
+ });
4656
+ }
4657
+ service.getStateStore().addEvidenceEvent({
4658
+ type: 'model_passport_created',
4659
+ details: {
4660
+ draftId,
4661
+ gaid: publishedGaid,
4662
+ passportId: passport.id ?? null,
4663
+ passportType: passport.passportType ?? null,
4664
+ workspaceId: passport.workspaceId ?? null,
4665
+ projectId: passport.projectId ?? null,
4666
+ backendStatus: publishResult.status,
4667
+ },
4668
+ });
4669
+ service.getStateStore().addEvidenceEvent({
4670
+ type: 'passport_bound',
4671
+ details: {
4672
+ draftId,
4673
+ gaid: publishedGaid,
4674
+ },
4675
+ });
4676
+ service.recordC2LifecycleEvent({
4677
+ eventType: 'connect_passport_bound',
4678
+ passportGaid: publishedGaid,
4679
+ workspaceId: String(passport.workspaceId || state.workspace_binding.workspaceId || '').trim() || null,
4680
+ projectId: String(passport.projectId || state.project_binding.projectId || '').trim() || null,
4681
+ metadata: {
4682
+ draft_id: draftId,
4683
+ passport_id: passport.id ?? null,
4684
+ connection_status: passport.connectionStatus ?? null,
4685
+ },
4686
+ });
4687
+ service.markBuildSessionDraftPublished(draftId, publishedGaid, String(passport.workspaceId || state.workspace_binding.workspaceId || '').trim() || null, String(passport.projectId || state.project_binding.projectId || '').trim() || null);
4688
+ console.log('Draft published successfully.');
4689
+ if (publishedGaid) {
4690
+ console.log(`Passport GAID: ${publishedGaid}`);
4691
+ }
4692
+ return;
4693
+ }
4694
+ if (command === 'scan' || command === 'discover') {
4695
+ await runPublicScan();
4696
+ return;
4697
+ }
4698
+ if (command === 'login') {
4699
+ await runPublicLogin();
4700
+ return;
4701
+ }
4702
+ if (command === 'logout') {
4703
+ runPublicLogout();
4704
+ return;
4705
+ }
4706
+ if (command === 'doctor') {
4707
+ const checks = await service.runDoctorChecks();
4708
+ for (const check of checks) {
4709
+ console.log(`[${check.status}] ${check.name}: ${check.details}`);
4710
+ }
4711
+ if (checks.some((check) => check.status === 'FAIL')) {
4712
+ process.exitCode = 2;
4713
+ }
4714
+ return;
4715
+ }
4716
+ showUsage();
4717
+ process.exitCode = 1;
4718
+ }
4719
+ void run().catch((error) => {
4720
+ const message = error instanceof Error ? error.message : 'forkit_connect_cli_failed';
4721
+ console.error(`[forkit-connect] ${message}`);
4722
+ process.exitCode = 2;
4723
+ });
4724
+ //# sourceMappingURL=cli.js.map