forkit-connect 0.1.20 → 0.1.22

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 (2) hide show
  1. package/dist/cli.js +1046 -188
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -87,9 +87,9 @@ const PUBLIC_COMMANDS = [
87
87
  ['scan', 'Discover runtimes, models, and agents on this device'],
88
88
  ['inbox', 'Review the Smart Registration Inbox'],
89
89
  ['sync', 'Flush queued drafts and lifecycle metadata'],
90
- ['workspace', 'List, select, or inspect optional governed workspace/project scope'],
91
- ['runtime', 'Register or review governed runtimes for the current repo/worktree'],
92
- ['register', 'Register ready local models into the current scope'],
90
+ ['workspace', 'Optional governed workspace and project scope'],
91
+ ['runtime', 'Register or review local runtimes for this repo/worktree'],
92
+ ['register', 'Register ready local models'],
93
93
  ['ignore', 'Deny one detected local model on this device'],
94
94
  ['doctor', 'Run local environment diagnostics'],
95
95
  ];
@@ -117,86 +117,42 @@ const ADVANCED_COMMAND_GROUPS = [
117
117
  ['notify', 'Notification preview and delivery controls'],
118
118
  ];
119
119
  function usage() {
120
- console.log('Usage: forkit-connect <init|login|logout|status|changes|start|stop|scan|inbox|sync|workspace|runtime|register|ignore|doctor> [options]');
121
- console.log(' forkit-connect workspace <list|select|create|status> [options]');
122
- console.log(' forkit-connect runtime <register|review|status> [options]');
123
- console.log('Public commands:');
120
+ printCliHeader('CLI', 'Precise local review, runtime detection, and registration handoff.');
121
+ console.log(cliKeyLine('usage', 'forkit-connect <command> [options]'));
122
+ console.log(cliKeyLine('scope', 'forkit-connect workspace <list|select|create|status>'));
123
+ console.log(cliKeyLine('runtime', 'forkit-connect runtime <register|review|status>'));
124
+ printCliSection('Core');
124
125
  for (const [command, description] of PUBLIC_COMMANDS) {
125
- console.log(` ${command.padEnd(10)} ${description}`);
126
- }
127
- console.log('Options:');
128
- console.log(' --description <text> Optional workspace description used by workspace create');
129
- console.log(' --project-name <text> Project name used by workspace create/select');
130
- console.log(' --project-description <text> Optional project description used by workspace create/select');
131
- console.log(' --json Print machine-readable output when supported');
132
- console.log(' --name <value> Runtime or metric name depending on command');
133
- console.log(' --model <value> Model name used by register');
134
- console.log(' --entrypoint <path> Relative project path used by runtime register');
135
- console.log(' --source-url <url> Source URL override used by runtime register');
136
- console.log(' --subject-type <type> Runtime subject type used by runtime register');
137
- console.log(' --deployment-environment <value> Runtime environment used by runtime register');
138
- console.log(' --show-key Print the raw runtime API key when one is created');
139
- console.log(' --no-api-key Skip automatic runtime API key creation');
140
- console.log(' --dry-run Show the inferred runtime payload without creating it');
141
- console.log(' --all-ready Register every ready local model in the current scope');
142
- console.log(' --ignore-model <name> Deny a detected model on this device with --ignore-digest');
143
- console.log(' --ignore-digest <sha> Digest used with --ignore-model');
144
- console.log(' --digest <sha> Digest used by ignore when the model name is ambiguous');
145
- console.log(' --interval-seconds <n> Override daemon scan interval');
146
- console.log(' --no-browser Do not auto-open verification URL during login');
147
- console.log(' --advanced-help Show internal/engineering commands');
126
+ console.log(cliKeyLine(command, description));
127
+ }
128
+ printCliSection('Flags');
129
+ console.log(cliKeyLine('--json', 'Machine-readable output when supported'));
130
+ console.log(cliKeyLine('--all-ready', 'Register every ready local model in the current scope'));
131
+ console.log(cliKeyLine('--model', 'Model name used by register'));
132
+ console.log(cliKeyLine('--entrypoint', 'Relative project path used by runtime register'));
133
+ console.log(cliKeyLine('--dry-run', 'Show inferred runtime payload without creating it'));
134
+ console.log(cliKeyLine('--no-browser', 'Do not auto-open browser verification during login'));
135
+ console.log(cliKeyLine('--plain', 'Disable the interactive terminal table surface'));
136
+ console.log(cliKeyLine('--advanced-help', 'Show internal and engineering commands'));
148
137
  }
149
138
  function advancedUsage() {
150
139
  usage();
151
- console.log('');
152
- console.log('Advanced commands:');
153
- console.log(' forkit-connect connect <modelNameOrDiscoveryHash>');
154
- console.log(' forkit-connect connect <start|init|status|inbox|services|permissions|handoff|evolution review|runtime review|runtime status>');
155
- console.log(' forkit-connect runtime target <add|list|remove|status> [options]');
156
- console.log(' forkit-connect runtime observe --gaid <gaid> [options]');
157
- console.log(' forkit-connect <review|workspaces|projects|bind|drafts|publish|bound|heartbeat|update-check|config|daemon|pulse|c2|train|agent|tray|notify> [options]');
140
+ printCliSection('Advanced');
141
+ console.log(cliKeyLine('connect', 'forkit-connect connect <modelNameOrDiscoveryHash>'));
142
+ console.log(cliKeyLine('connect', 'forkit-connect connect <start|init|status|inbox|services|permissions|handoff|evolution review|runtime review|runtime status>'));
143
+ console.log(cliKeyLine('targets', 'forkit-connect runtime target <add|list|remove|status>'));
144
+ console.log(cliKeyLine('observe', 'forkit-connect runtime observe --gaid <gaid>'));
145
+ console.log(cliKeyLine('more', 'forkit-connect <review|workspaces|projects|bind|drafts|publish|bound|heartbeat|update-check|config|daemon|pulse|c2|train|agent|tray|notify>'));
158
146
  for (const [command, description] of ADVANCED_COMMAND_GROUPS) {
159
- console.log(` ${command.padEnd(12)} ${description}`);
160
- }
161
- console.log('Advanced options:');
162
- console.log(' --session-ref <value> Store/update backend session reference');
163
- console.log(' --workspace <id> Store workspace binding');
164
- console.log(' --project <id> Store project binding');
165
- console.log(' --draft <id> Draft id used by publish');
166
- console.log(' --name <value> Model or metric name depending on command');
167
- console.log(' --framework <value> Training framework used by train init');
168
- console.log(' --task <value> Training task used by train init');
169
- console.log(' --dataset-ref <value> Safe dataset reference used by train init');
170
- console.log(' --type <value> Training lifecycle event type');
171
- console.log(' --ref <value> Dataset reference used by train dataset');
172
- console.log(' --change <value> Dataset change type used by train dataset');
173
- console.log(' --value <number> Metric value used by train metric');
174
- console.log(' --version <value> Version name used by train version');
175
- console.log(' --reason <value> Version or lifecycle reason');
176
- console.log(' --path <value> Artifact path used by train artifact');
177
- console.log(' --hash-artifact Hash artifact contents for train artifact');
178
- console.log(' --heartbeat-gaid <gaid> Queue heartbeat runtime signal event for GAID');
179
- console.log(' --heartbeat-key <key> API key used for heartbeat runtime signal event');
180
- console.log(' Also used by: c2 set-key (stores key + backfills events)');
181
- console.log(' --gaid <gaid> Runtime or passport GAID used by runtime observe and c2 run-log emit');
182
- console.log(' --api-key <key> Runtime signal API key used by runtime observe and c2 run-log emit');
183
- console.log(' --repo-root <path> Repo root used by runtime target add (defaults to current repo/worktree)');
184
- console.log(' --provider <name> Provider label used by c2 run-log emit');
185
- console.log(' --service-name <name> Service/agent/workflow label used by c2 run-log emit');
186
- console.log(' --prompt-tokens <n> Prompt tokens used by c2 run-log emit');
187
- console.log(' --completion-tokens <n> Completion tokens used by c2 run-log emit');
188
- console.log(' --client-name <name> Optional repo/runtime client label used by c2 run-log emit');
189
- console.log(' --command-label <name> Operator label used by runtime observe and c2 run-log emit');
190
- console.log(' --actor-labels <csv> Actor labels used by c2 run-log emit (for example: Codex,Claude)');
191
- console.log(' --task-labels <csv> Task or chat labels used by c2 run-log emit');
192
- console.log(' --folder-labels <csv> Relative folder labels used by c2 run-log emit');
193
- console.log(' --file-labels <csv> Relative file labels used by c2 run-log emit');
194
- console.log(' --model-labels <csv> Optional model labels used by c2 run-log emit');
195
- console.log(' --cpu-percent <n> Scoped CPU percentage used by c2 run-log emit');
196
- console.log(' --memory-mb <n> Scoped memory usage used by c2 run-log emit');
197
- console.log(' --vram-mb <n> Device VRAM usage used by c2 run-log emit');
198
- console.log(' --emit-ambient Include ambient tool detections in runtime observe JSON output');
199
- console.log(' --draft-only Allow draft creation even when governed publish capacity is full');
147
+ console.log(cliKeyLine(command, description));
148
+ }
149
+ printCliSection('Advanced Flags');
150
+ console.log(cliKeyLine('--session-ref', 'Store or update backend session reference'));
151
+ console.log(cliKeyLine('--workspace', 'Store workspace binding'));
152
+ console.log(cliKeyLine('--project', 'Store project binding'));
153
+ console.log(cliKeyLine('--gaid', 'Runtime or passport GAID used by runtime observe'));
154
+ console.log(cliKeyLine('--api-key', 'Runtime signal API key used by runtime observe'));
155
+ console.log(cliKeyLine('--draft-only', 'Allow private continuation when governed publish is blocked'));
200
156
  }
201
157
  function showUsage() {
202
158
  if (hasFlag('--advanced-help')) {
@@ -760,6 +716,13 @@ function formatScopeReferenceLabel(value, kind) {
760
716
  }
761
717
  return `selected on this device (${shortId(normalized)})`;
762
718
  }
719
+ function formatScopeDetailValue(value, kind) {
720
+ const normalized = String(value || '').trim();
721
+ if (!normalized) {
722
+ return kind === 'workspace' ? 'not selected' : 'not selected';
723
+ }
724
+ return `${kind} ${shortId(normalized)}`;
725
+ }
763
726
  function printUpdateCheckLines(lines, useErrorStream = false) {
764
727
  for (const line of lines) {
765
728
  if (useErrorStream) {
@@ -846,6 +809,207 @@ function printConnectInit(result) {
846
809
  function printJson(value) {
847
810
  console.log(JSON.stringify(value, null, 2));
848
811
  }
812
+ const CLI_LABEL_WIDTH = 16;
813
+ const CLI_WIDTH_FALLBACK = 88;
814
+ const ANSI_ESCAPE_PATTERN = /\u001B\[[0-9;]*m/g;
815
+ function canRenderCliColor() {
816
+ return Boolean(node_process_1.stdout.isTTY && process.env.NO_COLOR !== '1' && process.env.TERM !== 'dumb');
817
+ }
818
+ function paintCli(text, code) {
819
+ if (!canRenderCliColor()) {
820
+ return text;
821
+ }
822
+ return `\u001B[${code}m${text}\u001B[0m`;
823
+ }
824
+ function cliBold(text) {
825
+ return paintCli(text, '1');
826
+ }
827
+ function cliDim(text) {
828
+ return paintCli(text, '2');
829
+ }
830
+ function cliAccent(text) {
831
+ return paintCli(text, '1;36');
832
+ }
833
+ function cliGood(text) {
834
+ return paintCli(text, '1;32');
835
+ }
836
+ function cliWarm(text) {
837
+ return paintCli(text, '1;33');
838
+ }
839
+ function cliDanger(text) {
840
+ return paintCli(text, '1;31');
841
+ }
842
+ function cliPlainWidth(text) {
843
+ return text.replace(ANSI_ESCAPE_PATTERN, '').length;
844
+ }
845
+ function truncateCliText(value, maxWidth) {
846
+ const normalized = String(value ?? '').trim();
847
+ if (normalized.length <= maxWidth) {
848
+ return normalized;
849
+ }
850
+ return `${normalized.slice(0, Math.max(0, maxWidth - 3)).trimEnd()}...`;
851
+ }
852
+ function getCliRenderWidth() {
853
+ const columns = node_process_1.stdout.columns;
854
+ if (Number.isFinite(columns) && Number(columns) > 0) {
855
+ return Math.max(72, Number(columns));
856
+ }
857
+ return CLI_WIDTH_FALLBACK;
858
+ }
859
+ function cliRule(char = '-') {
860
+ return cliDim(char.repeat(Math.max(24, Math.min(getCliRenderWidth(), CLI_WIDTH_FALLBACK))));
861
+ }
862
+ function cliTag(label, tone = 'muted') {
863
+ const rendered = `[${label}]`;
864
+ switch (tone) {
865
+ case 'accent':
866
+ return cliAccent(rendered);
867
+ case 'good':
868
+ return cliGood(rendered);
869
+ case 'warm':
870
+ return cliWarm(rendered);
871
+ case 'danger':
872
+ return cliDanger(rendered);
873
+ default:
874
+ return cliDim(rendered);
875
+ }
876
+ }
877
+ function cliKeyLine(label, value) {
878
+ return ` ${cliDim(`${label}`.padEnd(CLI_LABEL_WIDTH, ' '))} ${value}`;
879
+ }
880
+ function printCliHeader(title, subtitle) {
881
+ console.log(`[forkit-connect] ${cliBold(title)}`);
882
+ if (subtitle) {
883
+ console.log(cliDim(subtitle));
884
+ }
885
+ console.log(cliRule());
886
+ }
887
+ function printCliSection(title) {
888
+ console.log('');
889
+ console.log(cliBold(title.toUpperCase()));
890
+ }
891
+ function printCliEntry(title, options) {
892
+ const trimmedTitle = truncateCliText(title, 36);
893
+ const summaryWidth = Math.max(24, Math.min(48, getCliRenderWidth() - cliPlainWidth(trimmedTitle) - 22));
894
+ const pieces = [trimmedTitle];
895
+ if (options?.tag) {
896
+ pieces.push(cliTag(options.tag, options.tagTone ?? 'muted'));
897
+ }
898
+ if (options?.summary) {
899
+ pieces.push(cliDim('->'));
900
+ pieces.push(truncateCliText(options.summary, summaryWidth));
901
+ }
902
+ console.log(` > ${pieces.join(' ')}`);
903
+ if (options?.meta) {
904
+ console.log(` ${cliDim(options.meta)}`);
905
+ }
906
+ }
907
+ function joinCliSummary(parts) {
908
+ return parts.filter((value) => Boolean(value && String(value).trim())).join(cliDim(' | '));
909
+ }
910
+ function formatCliCompactCount(count, singular, plural) {
911
+ const noun = count === 1 ? singular : (plural || `${singular}s`);
912
+ return `${count} ${noun}`;
913
+ }
914
+ function formatCliAge(seconds) {
915
+ if (!Number.isFinite(seconds) || seconds <= 0) {
916
+ return 'fresh';
917
+ }
918
+ if (seconds >= 86400) {
919
+ return `${Math.round(seconds / 86400)}d old`;
920
+ }
921
+ if (seconds >= 3600) {
922
+ return `${Math.round(seconds / 3600)}h old`;
923
+ }
924
+ if (seconds >= 60) {
925
+ return `${Math.round(seconds / 60)}m old`;
926
+ }
927
+ return `${Math.round(seconds)}s old`;
928
+ }
929
+ function formatCliScopeLabel(workspaceId, projectId) {
930
+ const workspace = String(workspaceId || '').trim();
931
+ const project = String(projectId || '').trim();
932
+ if (workspace && project) {
933
+ return `${shortId(workspace)} / ${shortId(project)}`;
934
+ }
935
+ if (workspace) {
936
+ return `${shortId(workspace)} / project needed`;
937
+ }
938
+ return 'solo / no workspace';
939
+ }
940
+ function summarizeReviewQueueCounts(values) {
941
+ const parts = [
942
+ values.draftFirstCount ? formatCliCompactCount(values.draftFirstCount, 'private review') : null,
943
+ values.readyCount ? formatCliCompactCount(values.readyCount, 'ready item') : null,
944
+ values.needsReviewCount ? formatCliCompactCount(values.needsReviewCount, 'review item') : null,
945
+ values.connectedCount ? formatCliCompactCount(values.connectedCount, 'connected item') : null,
946
+ ];
947
+ const joined = joinCliSummary(parts);
948
+ return joined || 'clear';
949
+ }
950
+ function formatCliEvidenceGlyph(confidence) {
951
+ switch (String(confidence || '').trim().toLowerCase()) {
952
+ case 'high':
953
+ return '●●●';
954
+ case 'medium':
955
+ return '●●○';
956
+ case 'low':
957
+ return '●○○';
958
+ default:
959
+ return '○○○';
960
+ }
961
+ }
962
+ function formatCliEvidenceLabel(confidence) {
963
+ const normalized = String(confidence || '').trim().toLowerCase();
964
+ switch (normalized) {
965
+ case 'high':
966
+ return 'high';
967
+ case 'medium':
968
+ return 'medium';
969
+ case 'low':
970
+ return 'low';
971
+ default:
972
+ return normalized || 'unknown';
973
+ }
974
+ }
975
+ function formatCliEvidenceCell(confidence) {
976
+ return `${formatCliEvidenceGlyph(confidence)} ${formatCliEvidenceLabel(confidence)}`;
977
+ }
978
+ function formatCliDetailLine(label, value, labelWidth = 12) {
979
+ const safeLabel = `${label}:`;
980
+ const paddedLabel = `${safeLabel}${' '.repeat(Math.max(1, labelWidth - safeLabel.length))}`;
981
+ const normalizedValue = value == null
982
+ ? ''
983
+ : typeof value === 'string'
984
+ ? value.trim()
985
+ : String(value);
986
+ return `${paddedLabel}${normalizedValue || normalizedValue === '0' ? normalizedValue : 'n/a'}`;
987
+ }
988
+ function formatRuntimeRecommendedActionLabel(action) {
989
+ const normalized = String(action || '').trim().toLowerCase();
990
+ if (normalized === 'link an agent to a model on this runtime.') {
991
+ return 'Link agent to model';
992
+ }
993
+ if (normalized === 'connect a model served by this runtime.') {
994
+ return 'Register served model';
995
+ }
996
+ if (normalized === 'check whether this runtime should be running locally.') {
997
+ return 'Check local runtime';
998
+ }
999
+ return truncateCliText(action, 36);
1000
+ }
1001
+ function getDraftFirstCountFromInbox(inbox) {
1002
+ return inbox.groups.ready_to_connect.filter((item) => (item.item_type === 'model'
1003
+ && item.recommended_action === 'create_passport_draft'
1004
+ && String(item.details_received_automatically.registration_flow_state || '').trim() === 'private_draft_required')).length;
1005
+ }
1006
+ function splitReadyInboxItems(inbox) {
1007
+ const privateReview = inbox.groups.ready_to_connect.filter((item) => (item.item_type === 'model'
1008
+ && item.recommended_action === 'create_passport_draft'
1009
+ && String(item.details_received_automatically.registration_flow_state || '').trim() === 'private_draft_required'));
1010
+ const ready = inbox.groups.ready_to_connect.filter((item) => !privateReview.includes(item));
1011
+ return { privateReview, ready };
1012
+ }
849
1013
  const INTERACTIVE_LABEL_WIDTH = 24;
850
1014
  const INTERACTIVE_DISCOVERY_TIMEOUT_MS = 800;
851
1015
  const INTERACTIVE_BINDING_TIMEOUT_MS = 800;
@@ -1235,42 +1399,65 @@ function printCollectedChanges(service, limit) {
1235
1399
  for (const event of state.evidence_events) {
1236
1400
  evidenceTypeCounts.set(event.type, (evidenceTypeCounts.get(event.type) || 0) + 1);
1237
1401
  }
1238
- console.log('[forkit-connect] Collected changes');
1239
- console.log(`- state_dir=${paths.stateDir}`);
1240
- console.log(`- state_file=${paths.stateFile}`);
1241
- console.log(`- evidence_events=${state.evidence_events.length}`);
1242
- console.log(`- pulse_events=${state.pulse_events.length}`);
1243
- console.log(`- sync_queue=${state.sync_queue.length}`);
1244
- console.log(`- pending_reviews=${state.pending_reviews.length}`);
1245
- console.log(`- evolution_candidates=${state.evolution_candidates.length}`);
1246
- console.log(`- c2_events=${state.c2_events.length}`);
1247
- console.log(`- detected_models=${state.detected_models.length}`);
1248
- console.log(`- detected_runtimes=${state.detected_runtimes.length}`);
1249
- console.log(`- detected_agents=${state.detected_agents.length}`);
1250
- console.log('[forkit-connect] Some collected changes are local advisory telemetry and review evidence; only synced queue items become backend-authoritative.');
1402
+ printCliHeader('Changes', 'Local evidence, queue, and runtime activity collected on this device.');
1403
+ console.log(cliKeyLine('state dir', paths.stateDir));
1404
+ console.log(cliKeyLine('state file', paths.stateFile));
1405
+ console.log(cliKeyLine('summary', joinCliSummary([
1406
+ formatCliCompactCount(state.evidence_events.length, 'evidence event'),
1407
+ formatCliCompactCount(state.pulse_events.length, 'pulse event'),
1408
+ formatCliCompactCount(state.sync_queue.length, 'queued sync'),
1409
+ formatCliCompactCount(state.pending_reviews.length, 'pending review'),
1410
+ ])));
1411
+ console.log(cliKeyLine('local', joinCliSummary([
1412
+ formatCliCompactCount(state.detected_models.length, 'model'),
1413
+ formatCliCompactCount(state.detected_runtimes.length, 'runtime'),
1414
+ formatCliCompactCount(state.detected_agents.length, 'agent'),
1415
+ formatCliCompactCount(state.c2_events.length, 'c2 event'),
1416
+ ])));
1417
+ console.log(cliKeyLine('note', 'Only synced queue items become backend-authoritative.'));
1251
1418
  const sortedEvidenceTypes = [...evidenceTypeCounts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
1252
1419
  if (sortedEvidenceTypes.length > 0) {
1253
- console.log('[forkit-connect] Evidence types observed:');
1420
+ printCliSection('Evidence Types');
1254
1421
  for (const [type, count] of sortedEvidenceTypes) {
1255
- console.log(`- ${type}: ${count}`);
1422
+ printCliEntry(type, {
1423
+ summary: formatCliCompactCount(count, 'event'),
1424
+ });
1256
1425
  }
1257
1426
  }
1258
1427
  if (recentEvidence.length > 0) {
1259
- console.log(`[forkit-connect] Recent evidence events showing=${recentEvidence.length} limit=${limit}`);
1428
+ printCliSection(`Recent Evidence ${cliTag(String(recentEvidence.length), 'accent')}`);
1260
1429
  for (const event of recentEvidence) {
1261
- console.log(`- ${formatTimestamp(event.createdAt)} | ${event.type}${formatEvidenceDetails(event.details)}`);
1430
+ printCliEntry(event.type, {
1431
+ summary: formatTimestamp(event.createdAt),
1432
+ meta: truncateCliText(formatEvidenceDetails(event.details).replace(/^\s*\|\s*/, ''), 88),
1433
+ });
1262
1434
  }
1263
1435
  }
1264
1436
  if (recentPulse.length > 0) {
1265
- console.log(`[forkit-connect] Recent runtime signal history showing=${recentPulse.length} limit=${limit}`);
1437
+ printCliSection(`Runtime Signals ${cliTag(String(recentPulse.length), 'good')}`);
1266
1438
  for (const event of recentPulse) {
1267
- console.log(`- ${event.measured_at} | runtime=${event.runtime_name} | model=${event.model_name || 'n/a'} | pulse=${event.pulse_status} | registration=${formatPulseRegistrationLabel(event)}`);
1439
+ printCliEntry(event.runtime_name, {
1440
+ summary: event.pulse_status,
1441
+ meta: joinCliSummary([
1442
+ formatTimestamp(event.measured_at),
1443
+ event.model_name ? `model ${event.model_name}` : null,
1444
+ formatPulseRegistrationLabel(event),
1445
+ ]),
1446
+ });
1268
1447
  }
1269
1448
  }
1270
1449
  if (state.sync_queue.length > 0) {
1271
- console.log('[forkit-connect] Pending sync queue:');
1450
+ printCliSection(`Pending Sync ${cliTag(String(Math.min(state.sync_queue.length, limit)), 'warm')}`);
1272
1451
  for (const item of state.sync_queue.slice(0, limit)) {
1273
- 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'}`);
1452
+ printCliEntry(item.type, {
1453
+ summary: item.endpoint,
1454
+ meta: joinCliSummary([
1455
+ `attempts ${item.attempts}`,
1456
+ `created ${formatTimestamp(item.createdAt)}`,
1457
+ item.nextRetryAt ? `retry ${formatTimestamp(item.nextRetryAt)}` : null,
1458
+ item.lastError ? `error ${truncateCliText(item.lastError, 48)}` : null,
1459
+ ]),
1460
+ });
1274
1461
  }
1275
1462
  }
1276
1463
  }
@@ -1304,14 +1491,25 @@ function printReviewSnapshot(snapshot) {
1304
1491
  'shadow_candidate',
1305
1492
  'provider_unavailable',
1306
1493
  ];
1307
- console.log(`[forkit-connect] Review items=${snapshot.total}`);
1494
+ printCliHeader('Review', `${formatCliCompactCount(snapshot.total, 'local item')} across models and runtimes.`);
1308
1495
  for (const status of order) {
1309
1496
  const entries = snapshot.groups[status];
1310
1497
  if (entries.length === 0)
1311
1498
  continue;
1312
- console.log(`${formatReviewStatusLabel(status)}: ${entries.length}`);
1499
+ printCliSection(`${formatReviewStatusLabel(status)} ${cliTag(String(entries.length), status === 'bound_passport' ? 'good' : status === 'provider_unavailable' ? 'danger' : 'warm')}`);
1313
1500
  for (const entry of entries) {
1314
- 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)}`);
1501
+ printCliEntry(formatCliReviewSubject(entry), {
1502
+ tag: entry.kind,
1503
+ tagTone: entry.kind === 'provider' ? 'danger' : entry.kind === 'process' ? 'warm' : 'accent',
1504
+ summary: formatReviewSuggestedActionCompact(entry.suggested_action),
1505
+ meta: joinCliSummary([
1506
+ formatCliReviewRuntime(entry),
1507
+ entry.discovery_hash ? `discovery ${shortId(entry.discovery_hash)}` : null,
1508
+ entry.registration_key ? `registration ${shortId(entry.registration_key)}` : null,
1509
+ entry.passport_gaid ? `passport ${shortId(entry.passport_gaid)}` : null,
1510
+ formatReviewEvidence(entry).replace(/^\s*\|\s*/, ''),
1511
+ ]),
1512
+ });
1315
1513
  }
1316
1514
  }
1317
1515
  }
@@ -1340,17 +1538,93 @@ function printTrainStatus(status) {
1340
1538
  }
1341
1539
  function printAgentReview(snapshot) {
1342
1540
  const order = ['new_agent', 'known_agent', 'linked_agent', 'inactive_agent', 'unknown_ai_tool'];
1343
- console.log(`[forkit-connect] Agent review items=${snapshot.total}`);
1541
+ printCliHeader('Agent Review', `${formatCliCompactCount(snapshot.total, 'agent item')} detected on this device.`);
1344
1542
  for (const status of order) {
1345
1543
  const entries = snapshot.groups[status];
1346
1544
  if (entries.length === 0)
1347
1545
  continue;
1348
- console.log(`${status}: ${entries.length}`);
1546
+ const tone = status === 'linked_agent' ? 'good' : status === 'inactive_agent' ? 'danger' : status === 'unknown_ai_tool' ? 'muted' : 'warm';
1547
+ const label = status === 'new_agent'
1548
+ ? 'New agents'
1549
+ : status === 'known_agent'
1550
+ ? 'Needs link'
1551
+ : status === 'linked_agent'
1552
+ ? 'Linked agents'
1553
+ : status === 'inactive_agent'
1554
+ ? 'Inactive agents'
1555
+ : 'Unknown AI tools';
1556
+ printCliSection(`${label} ${cliTag(String(entries.length), tone)}`);
1349
1557
  for (const entry of entries) {
1350
- 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)}`);
1558
+ printCliEntry(entry.agent_name, {
1559
+ tag: entry.agent_type.replaceAll('_', ' '),
1560
+ tagTone: entry.agent_type === 'unknown_ai_tool' ? 'muted' : 'accent',
1561
+ summary: formatAgentSuggestedActionCompact(entry.suggested_action),
1562
+ meta: joinCliSummary([
1563
+ entry.status.replaceAll('_', ' '),
1564
+ entry.linked_model_name ? `model ${entry.linked_model_name}` : null,
1565
+ entry.linked_passport_gaid ? `passport ${shortId(entry.linked_passport_gaid)}` : null,
1566
+ formatReviewEvidence(entry).replace(/^\s*\|\s*/, ''),
1567
+ ]),
1568
+ });
1351
1569
  }
1352
1570
  }
1353
1571
  }
1572
+ function printWorkspaceListSurface(workspaces, options) {
1573
+ printCliHeader('Workspaces', `${formatCliCompactCount(workspaces.length, 'accessible workspace')} on this account.`);
1574
+ const identityBits = [
1575
+ options?.displayName || null,
1576
+ options?.accountEmail || null,
1577
+ options?.platformRole ? `role ${options.platformRole}` : null,
1578
+ ];
1579
+ if (identityBits.some(Boolean)) {
1580
+ console.log(cliKeyLine('account', joinCliSummary(identityBits)));
1581
+ }
1582
+ if (options?.accountLimits) {
1583
+ console.log(cliKeyLine('plan', options.accountLimits.planName));
1584
+ console.log(cliKeyLine('capacity', joinCliSummary([
1585
+ formatRemainingLimit(options.accountLimits.workspaceLimit, options.accountLimits.workspacesUsed, 'workspace'),
1586
+ formatRemainingLimit(options.accountLimits.projectLimit, options.accountLimits.projectsUsed, 'project'),
1587
+ ])));
1588
+ }
1589
+ if (workspaces.length === 0) {
1590
+ console.log(cliKeyLine('next', 'Create a workspace from forkit-connect workspace create.'));
1591
+ return;
1592
+ }
1593
+ printCliSection('Accessible');
1594
+ for (const workspace of workspaces) {
1595
+ printCliEntry(String(workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace', {
1596
+ tag: String(workspace.role || 'unknown').trim() || 'unknown',
1597
+ tagTone: 'accent',
1598
+ summary: formatWorkspaceStateCell(workspace),
1599
+ meta: joinCliSummary([
1600
+ String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown'),
1601
+ workspace.verificationStatus ? `verification ${workspace.verificationStatus}` : null,
1602
+ workspace.description ? truncateCliText(workspace.description, 52) : null,
1603
+ ]),
1604
+ });
1605
+ }
1606
+ }
1607
+ function printProjectListSurface(projects, workspaceId) {
1608
+ printCliHeader('Projects', `${formatCliCompactCount(projects.length, 'project')} inside workspace ${shortId(workspaceId)}.`);
1609
+ if (projects.length === 0) {
1610
+ console.log(cliKeyLine('next', 'Create the first project with forkit-connect workspace select --workspace <id> --project-name "<name>".'));
1611
+ return;
1612
+ }
1613
+ printCliSection('Available');
1614
+ for (const project of projects) {
1615
+ const passportCount = Number(project.passportCount);
1616
+ printCliEntry(String(project.name || 'Unnamed project').trim() || 'Unnamed project', {
1617
+ tag: String(project.status || 'active').trim() || 'active',
1618
+ tagTone: 'good',
1619
+ summary: Number.isFinite(passportCount) && passportCount >= 0 ? `${passportCount} passports` : 'passport count unavailable',
1620
+ meta: joinCliSummary([
1621
+ String(project.id || 'unknown'),
1622
+ project.updatedAt ? `updated ${formatTimestamp(project.updatedAt)}` : null,
1623
+ project.description ? truncateCliText(project.description, 52) : null,
1624
+ ]),
1625
+ });
1626
+ }
1627
+ }
1354
1628
  function printAgentStatus(status) {
1355
1629
  console.log(JSON.stringify({
1356
1630
  detected_agents: status.detectedAgents,
@@ -1365,23 +1639,47 @@ function printAgentLedger(summary) {
1365
1639
  console.log(JSON.stringify(summary, null, 2));
1366
1640
  }
1367
1641
  function printRuntimeReview(summary) {
1368
- console.log('[forkit-connect] Runtime review');
1369
- console.log(`- runtimes=${summary.total_runtimes}`);
1370
- for (const runtime of summary.runtimes) {
1371
- console.log(`- ${runtime.runtime_name} | ${runtime.status} | linked models=${runtime.linked_models_count} | linked agents=${runtime.linked_agents_count} | health=${runtime.health_status} | next=${runtime.recommended_action}`);
1642
+ const ready = summary.runtimes.filter((runtime) => runtime.status !== 'unavailable' && runtime.health_status === 'detected');
1643
+ const attention = summary.runtimes.filter((runtime) => !ready.includes(runtime));
1644
+ printCliHeader('Runtime Review', `${formatCliCompactCount(summary.total_runtimes, 'runtime')} on this device`);
1645
+ console.log(cliKeyLine('summary', joinCliSummary([
1646
+ formatCliCompactCount(ready.length, 'healthy runtime'),
1647
+ formatCliCompactCount(attention.length, 'runtime needs review', 'runtimes need review'),
1648
+ ])));
1649
+ for (const [label, items] of [['Healthy', ready], ['Needs review', attention]]) {
1650
+ if (items.length === 0) {
1651
+ continue;
1652
+ }
1653
+ printCliSection(`${label} ${cliTag(String(items.length), label === 'Healthy' ? 'good' : 'warm')}`);
1654
+ for (const runtime of items) {
1655
+ printCliEntry(runtime.runtime_name, {
1656
+ tag: runtime.status.replaceAll('_', ' '),
1657
+ tagTone: runtime.status === 'unavailable' ? 'warm' : 'good',
1658
+ meta: joinCliSummary([
1659
+ `health ${runtime.health_status}`,
1660
+ formatCliCompactCount(runtime.linked_models_count, 'model'),
1661
+ formatCliCompactCount(runtime.linked_agents_count, 'agent'),
1662
+ `next ${formatRuntimeRecommendedActionLabel(runtime.recommended_action)}`,
1663
+ ]),
1664
+ });
1665
+ }
1372
1666
  }
1373
1667
  }
1374
1668
  function printRuntimeStatus(status) {
1375
- console.log('[forkit-connect] Runtime status');
1376
- console.log(`- total=${status.total_runtimes}`);
1377
- console.log(`- online=${status.online_runtimes}`);
1378
- console.log(`- offline=${status.offline_runtimes}`);
1379
- console.log(`- linked=${status.linked_runtimes}`);
1380
- console.log(`- unlinked=${status.unlinked_runtimes}`);
1381
- console.log(`- needs attention=${status.unhealthy_runtimes}`);
1382
- console.log(`- pending runtime sync=${status.c2_pending_count}`);
1669
+ printCliHeader('Runtime Status');
1670
+ console.log(cliKeyLine('runtimes', joinCliSummary([
1671
+ formatCliCompactCount(status.total_runtimes, 'total'),
1672
+ formatCliCompactCount(status.online_runtimes, 'online runtime'),
1673
+ formatCliCompactCount(status.offline_runtimes, 'offline runtime'),
1674
+ ])));
1675
+ console.log(cliKeyLine('linking', joinCliSummary([
1676
+ formatCliCompactCount(status.linked_runtimes, 'linked runtime'),
1677
+ formatCliCompactCount(status.unlinked_runtimes, 'unlinked runtime'),
1678
+ formatCliCompactCount(status.unhealthy_runtimes, 'runtime needs review', 'runtimes need review'),
1679
+ ])));
1680
+ console.log(cliKeyLine('sync', `${status.c2_pending_count} pending runtime events`));
1383
1681
  if (status.latest_runtime_lifecycle_event) {
1384
- console.log(`- latest event=${status.latest_runtime_lifecycle_event}`);
1682
+ console.log(cliKeyLine('latest', truncateCliText(status.latest_runtime_lifecycle_event, 60)));
1385
1683
  }
1386
1684
  }
1387
1685
  function printRuntimeTargetList(service, options) {
@@ -1437,24 +1735,369 @@ function formatSmartInboxActionLabel(item) {
1437
1735
  }
1438
1736
  return formatSmartInboxActionValue(item.recommended_action, item.item_type, String(item.details_received_automatically.connectable_model_name || '').trim());
1439
1737
  }
1738
+ function getInboxGroupTone(label) {
1739
+ switch (label) {
1740
+ case 'Private review':
1741
+ return 'accent';
1742
+ case 'Ready to connect':
1743
+ return 'good';
1744
+ case 'Needs review':
1745
+ return 'warm';
1746
+ case 'Denied on this device':
1747
+ return 'danger';
1748
+ default:
1749
+ return 'muted';
1750
+ }
1751
+ }
1752
+ function formatInboxItemMeta(item) {
1753
+ const match = item.matched_passport_gaid ? `match ${shortId(item.matched_passport_gaid)}` : null;
1754
+ const runtime = String(item.details_received_automatically.runtime_name || '').trim();
1755
+ const displayName = String(item.display_name || '').trim().toLowerCase();
1756
+ const sourceCandidate = item.item_type === 'runtime'
1757
+ ? String(item.details_received_automatically.source_endpoint_label || runtime || '').trim()
1758
+ : '';
1759
+ const sourceNormalized = sourceCandidate.toLowerCase();
1760
+ const runtimeNormalized = runtime.toLowerCase();
1761
+ const detectedSource = String(item.detected_source || '').trim().toLowerCase();
1762
+ const source = sourceCandidate && sourceNormalized !== displayName && sourceNormalized !== runtimeNormalized && sourceNormalized !== detectedSource
1763
+ ? sourceCandidate
1764
+ : null;
1765
+ const confidence = item.confidence !== 'high' ? `${item.confidence} confidence` : null;
1766
+ const parts = [source || null, confidence, match];
1767
+ const joined = joinCliSummary(parts);
1768
+ return joined || null;
1769
+ }
1440
1770
  function printInboxGroup(label, items) {
1441
1771
  if (items.length === 0)
1442
1772
  return;
1443
- console.log(`${label}: ${items.length}`);
1773
+ printCliSection(`${label} ${cliTag(String(items.length), getInboxGroupTone(label))}`);
1444
1774
  for (const item of items) {
1445
- 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)}` : ''}`);
1775
+ printCliEntry(item.display_name, {
1776
+ tag: item.item_type.replaceAll('_', ' '),
1777
+ tagTone: item.item_type === 'runtime' ? 'accent' : item.item_type === 'agent' ? 'warm' : 'muted',
1778
+ summary: formatSmartInboxActionLabel(item),
1779
+ meta: formatInboxItemMeta(item),
1780
+ });
1446
1781
  }
1447
1782
  }
1448
1783
  function printSmartInbox(inbox) {
1449
- console.log(`[forkit-connect] Smart Registration Inbox generated_at=${inbox.summary.generated_at}`);
1450
- console.log(`Governance authority: website | user_api_key_required=${String(inbox.user_api_key_required)}`);
1451
- printInboxGroup('Ready to Connect', inbox.groups.ready_to_connect);
1452
- printInboxGroup('Needs Confirmation', inbox.groups.needs_confirmation);
1453
- printInboxGroup('Connected', inbox.groups.connected);
1454
- printInboxGroup('Denied on This Device', inbox.groups.ignored);
1784
+ const draftFirstCount = getDraftFirstCountFromInbox(inbox);
1785
+ const { privateReview, ready } = splitReadyInboxItems(inbox);
1786
+ const freshness = inbox.summary.freshness_state ?? 'fresh';
1787
+ const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
1788
+ ? Number(inbox.summary.snapshot_age_seconds)
1789
+ : 0;
1790
+ printCliHeader('Inbox', `${formatTimestamp(inbox.summary.generated_at)} snapshot`);
1791
+ console.log(cliKeyLine('freshness', joinCliSummary([
1792
+ freshness,
1793
+ formatCliAge(ageSeconds),
1794
+ ])));
1795
+ console.log(cliKeyLine('queue', summarizeReviewQueueCounts({
1796
+ draftFirstCount,
1797
+ readyCount: ready.length,
1798
+ needsReviewCount: inbox.groups.needs_confirmation.length,
1799
+ connectedCount: inbox.groups.connected.length,
1800
+ })));
1455
1801
  if (inbox.summary.next_recommended_action) {
1456
- console.log(`Next recommended action: ${formatSmartInboxActionValue(inbox.summary.next_recommended_action)}`);
1802
+ console.log(cliKeyLine('next', formatSmartInboxActionValue(inbox.summary.next_recommended_action)));
1803
+ }
1804
+ printInboxGroup('Private review', privateReview);
1805
+ printInboxGroup('Needs review', inbox.groups.needs_confirmation);
1806
+ printInboxGroup('Ready to connect', ready);
1807
+ printInboxGroup('Connected', inbox.groups.connected);
1808
+ printInboxGroup('Denied on this device', inbox.groups.ignored);
1809
+ }
1810
+ function formatCliEvidenceSummary(confidence, sourceLabel) {
1811
+ const normalizedConfidence = String(confidence || '').trim().toLowerCase();
1812
+ if (normalizedConfidence === 'high' || normalizedConfidence === 'medium' || normalizedConfidence === 'low') {
1813
+ return formatCliEvidenceCell(normalizedConfidence);
1457
1814
  }
1815
+ const source = String(sourceLabel || '').trim();
1816
+ return source ? truncateCliText(source, 14) : 'local review';
1817
+ }
1818
+ function formatCliReviewSubject(entry) {
1819
+ if (entry.kind === 'process') {
1820
+ return String(entry.process_name || 'Unknown process').trim() || 'Unknown process';
1821
+ }
1822
+ return String(entry.model_name || entry.runtime_name || 'Unknown item').trim() || 'Unknown item';
1823
+ }
1824
+ function formatCliReviewRuntime(entry) {
1825
+ if (entry.kind === 'provider') {
1826
+ return String(entry.runtime_name || 'runtime').trim() || 'runtime';
1827
+ }
1828
+ return String(entry.runtime_name || 'local').trim() || 'local';
1829
+ }
1830
+ function formatReviewSuggestedActionCompact(action) {
1831
+ switch (action) {
1832
+ case 'connect_model':
1833
+ return 'Connect model';
1834
+ case 'review_drafts':
1835
+ return 'Open draft';
1836
+ case 'already_connected':
1837
+ return 'Already linked';
1838
+ case 'review/runtime':
1839
+ return 'Check runtime';
1840
+ case 'review/connect':
1841
+ case 'review_model':
1842
+ return 'Review item';
1843
+ default:
1844
+ return truncateCliText(action.replaceAll('_', ' '), 24);
1845
+ }
1846
+ }
1847
+ function buildDiscoveryInteractiveSections(snapshot) {
1848
+ const mapRows = (groupLabel, items) => items.map((entry) => ({ groupLabel, entry }));
1849
+ const sections = [
1850
+ { id: 'new', label: 'New', tone: 'accent', rows: mapRows('New models', snapshot.groups.new_unregistered) },
1851
+ { id: 'known', label: 'Known', tone: 'warm', rows: mapRows('Known models', snapshot.groups.known_unregistered) },
1852
+ { id: 'drafts', label: 'Drafts', tone: 'good', rows: mapRows('Pending drafts', snapshot.groups.pending_draft) },
1853
+ { id: 'linked', label: 'Linked', tone: 'muted', rows: mapRows('Connected', snapshot.groups.bound_passport) },
1854
+ { id: 'runtime', label: 'Runtime', tone: 'warm', rows: mapRows('Unconnected runtimes', snapshot.groups.shadow_candidate) },
1855
+ { id: 'unavailable', label: 'Offline', tone: 'danger', rows: mapRows('Unavailable runtimes', snapshot.groups.provider_unavailable) },
1856
+ ];
1857
+ return sections.filter((section) => section.rows.length > 0);
1858
+ }
1859
+ function buildDiscoveryInteractiveDetailLines(row) {
1860
+ const entry = row.entry;
1861
+ const lines = [
1862
+ formatCliDetailLine('Review', row.groupLabel),
1863
+ formatCliDetailLine('Item', formatCliReviewSubject(entry)),
1864
+ formatCliDetailLine('Runtime', formatCliReviewRuntime(entry)),
1865
+ formatCliDetailLine('Next', formatReviewSuggestedActionCompact(entry.suggested_action)),
1866
+ formatCliDetailLine('Evidence', formatCliEvidenceSummary(entry.confidence, entry.source_label)),
1867
+ ];
1868
+ if (entry.source_label) {
1869
+ lines.push(formatCliDetailLine('Source', entry.source_label));
1870
+ }
1871
+ if (entry.discovery_hash) {
1872
+ lines.push(formatCliDetailLine('Discovery', shortId(entry.discovery_hash)));
1873
+ }
1874
+ if (entry.registration_key) {
1875
+ lines.push(formatCliDetailLine('Registration', shortId(entry.registration_key)));
1876
+ }
1877
+ if (entry.draft_id) {
1878
+ lines.push(formatCliDetailLine('Draft', shortId(entry.draft_id)));
1879
+ }
1880
+ if (entry.passport_gaid) {
1881
+ lines.push(formatCliDetailLine('Passport', shortId(entry.passport_gaid)));
1882
+ }
1883
+ if (entry.reason) {
1884
+ lines.push(formatCliDetailLine('Reason', truncateCliText(entry.reason, 64)));
1885
+ }
1886
+ return lines;
1887
+ }
1888
+ async function maybeShowInteractiveDiscoveryReviewTable(snapshot) {
1889
+ return await viewInteractiveTable({
1890
+ title: '[forkit-connect] Review',
1891
+ subtitle: `${formatCliCompactCount(snapshot.total, 'local item')} across models and runtimes`,
1892
+ sections: buildDiscoveryInteractiveSections(snapshot),
1893
+ columns: [
1894
+ { header: 'Item', width: 30, render: (row) => formatCliReviewSubject(row.entry) },
1895
+ { header: 'Runtime', width: 18, render: (row) => formatCliReviewRuntime(row.entry) },
1896
+ { header: 'Next', width: 24, render: (row) => formatReviewSuggestedActionCompact(row.entry.suggested_action) },
1897
+ { header: 'Evidence', width: 14, render: (row) => formatCliEvidenceSummary(row.entry.confidence, row.entry.source_label) },
1898
+ ],
1899
+ detailLines: buildDiscoveryInteractiveDetailLines,
1900
+ detailTitle: (row) => `${row.entry.kind.toUpperCase()} SNAPSHOT`,
1901
+ emptyState: 'No local review items are available right now.',
1902
+ });
1903
+ }
1904
+ function formatAgentSuggestedActionCompact(action) {
1905
+ switch (action) {
1906
+ case 'none':
1907
+ return 'Already linked';
1908
+ case 'rescan_agent':
1909
+ return 'Rescan agent';
1910
+ case 'review_agent':
1911
+ return 'Review agent';
1912
+ case 'link_model':
1913
+ return 'Link model';
1914
+ case 'connect_agent':
1915
+ return 'Connect agent';
1916
+ default:
1917
+ return truncateCliText(action.replaceAll('_', ' '), 24);
1918
+ }
1919
+ }
1920
+ function buildAgentInteractiveSections(snapshot) {
1921
+ const rows = (groupLabel, items) => items.map((entry) => ({ groupLabel, entry }));
1922
+ const sections = [
1923
+ { id: 'new', label: 'New', tone: 'accent', rows: rows('New agents', snapshot.groups.new_agent) },
1924
+ { id: 'known', label: 'Needs link', tone: 'warm', rows: rows('Needs link', snapshot.groups.known_agent) },
1925
+ { id: 'linked', label: 'Linked', tone: 'good', rows: rows('Linked agents', snapshot.groups.linked_agent) },
1926
+ { id: 'inactive', label: 'Inactive', tone: 'danger', rows: rows('Inactive agents', snapshot.groups.inactive_agent) },
1927
+ { id: 'tooling', label: 'Tooling', tone: 'muted', rows: rows('Unknown AI tools', snapshot.groups.unknown_ai_tool) },
1928
+ ];
1929
+ return sections.filter((section) => section.rows.length > 0);
1930
+ }
1931
+ function buildAgentInteractiveDetailLines(row) {
1932
+ const entry = row.entry;
1933
+ const lines = [
1934
+ formatCliDetailLine('Review', row.groupLabel),
1935
+ formatCliDetailLine('Status', entry.status.replaceAll('_', ' ')),
1936
+ formatCliDetailLine('Next', formatAgentSuggestedActionCompact(entry.suggested_action)),
1937
+ formatCliDetailLine('Evidence', formatCliEvidenceSummary(entry.confidence, entry.source_label)),
1938
+ formatCliDetailLine('Agent ID', shortId(entry.agent_id)),
1939
+ ];
1940
+ if (entry.source_label) {
1941
+ lines.push(formatCliDetailLine('Source', entry.source_label));
1942
+ }
1943
+ if (entry.linked_model_name) {
1944
+ lines.push(formatCliDetailLine('Model', entry.linked_model_name));
1945
+ }
1946
+ if (entry.linked_passport_gaid) {
1947
+ lines.push(formatCliDetailLine('Passport', shortId(entry.linked_passport_gaid)));
1948
+ }
1949
+ if (entry.matched_terms?.length) {
1950
+ lines.push(formatCliDetailLine('Matched', truncateCliText(entry.matched_terms.join(', '), 64)));
1951
+ }
1952
+ return lines;
1953
+ }
1954
+ async function maybeShowInteractiveAgentReviewTable(snapshot) {
1955
+ return await viewInteractiveTable({
1956
+ title: '[forkit-connect] Agent Review',
1957
+ subtitle: `${formatCliCompactCount(snapshot.total, 'agent item')} discovered on this device`,
1958
+ sections: buildAgentInteractiveSections(snapshot),
1959
+ columns: [
1960
+ { header: 'Agent', width: 28, render: (row) => row.entry.agent_name },
1961
+ { header: 'Type', width: 18, render: (row) => row.entry.agent_type.replaceAll('_', ' ') },
1962
+ { header: 'State', width: 16, render: (row) => row.entry.status.replaceAll('_', ' ') },
1963
+ { header: 'Next', width: 22, render: (row) => formatAgentSuggestedActionCompact(row.entry.suggested_action) },
1964
+ ],
1965
+ detailLines: buildAgentInteractiveDetailLines,
1966
+ detailTitle: () => 'AGENT SNAPSHOT',
1967
+ emptyState: 'No agent review items are available right now.',
1968
+ });
1969
+ }
1970
+ function formatWorkspaceStateCell(workspace) {
1971
+ return [String(workspace.visibility || '').trim(), String(workspace.lifecycleStatus || '').trim()]
1972
+ .filter(Boolean)
1973
+ .join(' · ') || 'active';
1974
+ }
1975
+ function buildWorkspaceInteractiveDetailLines(row) {
1976
+ const workspace = row.workspace;
1977
+ const lines = [
1978
+ formatCliDetailLine('Workspace', String(workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace'),
1979
+ formatCliDetailLine('Role', String(workspace.role || 'unknown').trim() || 'unknown'),
1980
+ formatCliDetailLine('Visibility', String(workspace.visibility || 'not set').trim() || 'not set'),
1981
+ formatCliDetailLine('Lifecycle', String(workspace.lifecycleStatus || 'active').trim() || 'active'),
1982
+ formatCliDetailLine('Verification', String(workspace.verificationStatus || 'unverified').trim() || 'unverified'),
1983
+ formatCliDetailLine('ID', String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown').trim() || 'unknown'),
1984
+ ];
1985
+ if (workspace.ownerName) {
1986
+ lines.push(formatCliDetailLine('Owner', String(workspace.ownerName).trim()));
1987
+ }
1988
+ if (workspace.description) {
1989
+ lines.push(formatCliDetailLine('Notes', truncateCliText(workspace.description, 64)));
1990
+ }
1991
+ return lines;
1992
+ }
1993
+ async function maybeShowInteractiveWorkspaceTable(workspaces) {
1994
+ return await viewInteractiveTable({
1995
+ title: '[forkit-connect] Workspaces',
1996
+ subtitle: `${formatCliCompactCount(workspaces.length, 'accessible workspace')} on this account`,
1997
+ sections: [{ id: 'all', label: 'Accessible', tone: 'accent', rows: workspaces.map((workspace) => ({ workspace })) }],
1998
+ columns: [
1999
+ { header: 'Workspace', width: 28, render: (row) => String(row.workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace' },
2000
+ { header: 'Role', width: 14, render: (row) => String(row.workspace.role || 'unknown').trim() || 'unknown' },
2001
+ { header: 'State', width: 26, render: (row) => formatWorkspaceStateCell(row.workspace) },
2002
+ { header: 'ID', width: 14, render: (row) => shortId(String(row.workspace.id || row.workspace.gaid || row.workspace.passportGaid || 'unknown')) },
2003
+ ],
2004
+ detailLines: buildWorkspaceInteractiveDetailLines,
2005
+ detailTitle: () => 'WORKSPACE SNAPSHOT',
2006
+ emptyState: 'No workspaces are available right now.',
2007
+ });
2008
+ }
2009
+ function buildProjectInteractiveDetailLines(row) {
2010
+ const project = row.project;
2011
+ const passportCount = Number(project.passportCount);
2012
+ const lines = [
2013
+ formatCliDetailLine('Project', String(project.name || 'Unnamed project').trim() || 'Unnamed project'),
2014
+ formatCliDetailLine('Status', String(project.status || 'active').trim() || 'active'),
2015
+ formatCliDetailLine('Passports', Number.isFinite(passportCount) && passportCount >= 0 ? String(passportCount) : 'n/a'),
2016
+ formatCliDetailLine('Project ID', String(project.id || 'unknown').trim() || 'unknown'),
2017
+ formatCliDetailLine('Workspace', shortId(row.workspaceId || String(project.workspaceId || ''))),
2018
+ ];
2019
+ if (project.updatedAt) {
2020
+ lines.push(formatCliDetailLine('Updated', formatTimestamp(project.updatedAt)));
2021
+ }
2022
+ if (project.description) {
2023
+ lines.push(formatCliDetailLine('Notes', truncateCliText(project.description, 64)));
2024
+ }
2025
+ return lines;
2026
+ }
2027
+ async function maybeShowInteractiveProjectTable(projects, workspaceId) {
2028
+ return await viewInteractiveTable({
2029
+ title: '[forkit-connect] Projects',
2030
+ subtitle: `${formatCliCompactCount(projects.length, 'project')} inside workspace ${shortId(workspaceId)}`,
2031
+ sections: [{ id: 'all', label: 'Available', tone: 'accent', rows: projects.map((project) => ({ workspaceId, project })) }],
2032
+ columns: [
2033
+ { header: 'Project', width: 28, render: (row) => String(row.project.name || 'Unnamed project').trim() || 'Unnamed project' },
2034
+ { header: 'Status', width: 16, render: (row) => String(row.project.status || 'active').trim() || 'active' },
2035
+ { header: 'Passports', width: 12, render: (row) => {
2036
+ const passportCount = Number(row.project.passportCount);
2037
+ return Number.isFinite(passportCount) && passportCount >= 0 ? String(passportCount) : '-';
2038
+ }, align: 'right' },
2039
+ { header: 'Project ID', width: 14, render: (row) => shortId(String(row.project.id || 'unknown')) },
2040
+ ],
2041
+ detailLines: buildProjectInteractiveDetailLines,
2042
+ detailTitle: () => 'PROJECT SNAPSHOT',
2043
+ emptyState: 'No projects are available right now.',
2044
+ });
2045
+ }
2046
+ function buildInboxInteractiveSections(inbox) {
2047
+ const { privateReview, ready } = splitReadyInboxItems(inbox);
2048
+ const rows = (groupLabel, items) => (items.map((item) => ({ groupLabel, item })));
2049
+ const sections = [
2050
+ { id: 'review', label: 'Review', tone: 'warm', rows: rows('Needs review', inbox.groups.needs_confirmation) },
2051
+ { id: 'private', label: 'Private', tone: 'accent', rows: rows('Private review', privateReview) },
2052
+ { id: 'ready', label: 'Ready', tone: 'good', rows: rows('Ready to connect', ready) },
2053
+ { id: 'connected', label: 'Connected', tone: 'muted', rows: rows('Connected', inbox.groups.connected) },
2054
+ { id: 'denied', label: 'Denied', tone: 'danger', rows: rows('Denied on this device', inbox.groups.ignored) },
2055
+ ];
2056
+ return sections.filter((section) => section.rows.length > 0);
2057
+ }
2058
+ function buildInboxInteractiveDetailLines(row) {
2059
+ const item = row.item;
2060
+ const lines = [
2061
+ formatCliDetailLine('Review', row.groupLabel),
2062
+ formatCliDetailLine('Next', formatSmartInboxActionLabel(item)),
2063
+ formatCliDetailLine('Evidence', `${formatCliEvidenceLabel(item.confidence)} local evidence`),
2064
+ formatCliDetailLine('Source', item.detected_source),
2065
+ ];
2066
+ if (item.workspaceId || item.projectId) {
2067
+ lines.push(formatCliDetailLine('Workspace', formatScopeDetailValue(item.workspaceId, 'workspace')));
2068
+ lines.push(formatCliDetailLine('Project', formatScopeDetailValue(item.projectId, 'project')));
2069
+ }
2070
+ else {
2071
+ lines.push(formatCliDetailLine('Scope', 'solo / no workspace'));
2072
+ }
2073
+ if (item.matched_passport_gaid) {
2074
+ lines.push(formatCliDetailLine('Passport', item.matched_passport_gaid));
2075
+ }
2076
+ return lines;
2077
+ }
2078
+ async function maybeShowInteractiveInboxTable(inbox) {
2079
+ const freshness = inbox.summary.freshness_state ?? 'fresh';
2080
+ const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
2081
+ ? Number(inbox.summary.snapshot_age_seconds)
2082
+ : 0;
2083
+ return await viewInteractiveTable({
2084
+ title: '[forkit-connect] Inbox',
2085
+ subtitle: joinCliSummary([
2086
+ `${formatTimestamp(inbox.summary.generated_at)} snapshot`,
2087
+ freshness,
2088
+ formatCliAge(ageSeconds),
2089
+ ]),
2090
+ sections: buildInboxInteractiveSections(inbox),
2091
+ columns: [
2092
+ { header: 'Item', width: 32, render: (row) => row.item.display_name },
2093
+ { header: 'Type', width: 18, render: (row) => row.item.item_type.replaceAll('_', ' ') },
2094
+ { header: 'Next', width: 28, render: (row) => formatSmartInboxActionLabel(row.item) },
2095
+ { header: 'Evidence', width: 14, render: (row) => formatCliEvidenceCell(row.item.confidence) },
2096
+ ],
2097
+ detailLines: buildInboxInteractiveDetailLines,
2098
+ detailTitle: (row) => `${row.item.item_type.replaceAll('_', ' ').toUpperCase()} SNAPSHOT`,
2099
+ emptyState: 'No inbox items are available right now.',
2100
+ });
1458
2101
  }
1459
2102
  function printConnectStatusOverview(status) {
1460
2103
  const sessionMissing = !status.device_paired && status.binding_state === 'active' && !status.credential_reconnect_needed;
@@ -1483,20 +2126,28 @@ function printConnectStatusOverview(status) {
1483
2126
  }
1484
2127
  function printPublicStatusOverview(status) {
1485
2128
  const otherReadyCount = getOtherReadyCount(status.ready_to_connect_count, status.draft_first_count);
1486
- console.log('[forkit-connect] Status');
1487
- console.log(`- device=${status.device_paired ? 'paired' : 'approval pending'}`);
1488
- console.log(`- scope=${status.workspace_id && status.project_id ? `${status.workspace_id} / ${status.project_id}` : 'not selected'}`);
1489
- console.log(`- daemon=${status.daemon_status}`);
1490
- console.log(`- local inventory=models ${status.models_discovered} · agents ${status.agents_discovered} · runtimes ${status.runtimes_discovered}`);
1491
- console.log(`- review queue=private review ${status.draft_first_count} · ready ${otherReadyCount} · needs review ${status.needs_confirmation_count}`);
1492
- console.log(`- connected records=${status.connected_count}`);
1493
- console.log(`- pending runtime sync=${status.c2_sync_pending}`);
1494
- console.log(`- privacy=${status.privacy_mode}`);
2129
+ printCliHeader('Status');
2130
+ console.log(cliKeyLine('device', status.device_paired ? 'paired' : 'approval pending'));
2131
+ console.log(cliKeyLine('scope', formatCliScopeLabel(status.workspace_id, status.project_id)));
2132
+ console.log(cliKeyLine('daemon', status.daemon_status));
2133
+ console.log(cliKeyLine('privacy', status.privacy_mode));
2134
+ console.log(cliKeyLine('inventory', joinCliSummary([
2135
+ formatCliCompactCount(status.models_discovered, 'model'),
2136
+ formatCliCompactCount(status.agents_discovered, 'agent'),
2137
+ formatCliCompactCount(status.runtimes_discovered, 'runtime'),
2138
+ ])));
2139
+ console.log(cliKeyLine('queue', summarizeReviewQueueCounts({
2140
+ draftFirstCount: status.draft_first_count,
2141
+ readyCount: otherReadyCount,
2142
+ needsReviewCount: status.needs_confirmation_count,
2143
+ })));
2144
+ console.log(cliKeyLine('linked', formatCliCompactCount(status.connected_count, 'connected record')));
2145
+ console.log(cliKeyLine('sync', `${status.c2_sync_pending} pending runtime events`));
1495
2146
  if (status.lifecycle_note) {
1496
- console.log(`- note=${status.lifecycle_note}`);
2147
+ console.log(cliKeyLine('governed', truncateCliText(status.lifecycle_note, 88)));
1497
2148
  }
1498
2149
  if (!status.device_paired && status.binding_state === 'active') {
1499
- console.log('- warning=Local workspace/project scope exists, but the account session is missing. Re-login is required before governed actions can continue.');
2150
+ console.log(cliKeyLine('warning', 'Local governed scope exists, but login is still required.'));
1500
2151
  }
1501
2152
  }
1502
2153
  function getOtherReadyCount(readyToConnectCount, draftFirstCount) {
@@ -1510,34 +2161,65 @@ function printPublicStatusGuidance(status, sessionState) {
1510
2161
  : sessionState === 'unavailable'
1511
2162
  ? 'unverified'
1512
2163
  : 'login required';
1513
- console.log(`- account=${accountLabel}`);
2164
+ console.log(cliKeyLine('account', accountLabel));
1514
2165
  if (sessionState === 'missing') {
1515
- console.log('- note=Local discovery is working. Sign in next to pair this device with your Forkit.dev account.');
1516
- console.log('- next=run forkit-connect login');
2166
+ console.log(cliKeyLine('session', 'Local discovery is working. Sign in to pair this device.'));
2167
+ console.log(cliKeyLine('next', 'forkit-connect login'));
1517
2168
  if (status.binding_state === 'active' && status.workspace_id && status.project_id) {
1518
- console.log('- warning=This device has local workspace/project scope saved, but it is not an active account session yet.');
2169
+ console.log(cliKeyLine('scope', 'This device still has saved governed scope from an older session.'));
1519
2170
  }
1520
2171
  return;
1521
2172
  }
1522
2173
  if (sessionState === 'expired') {
1523
- console.log('- note=Your local device is initialized, but the account session needs to be renewed.');
1524
- console.log('- next=run forkit-connect login');
2174
+ console.log(cliKeyLine('session', 'The local device is ready, but the account session needs renewal.'));
2175
+ console.log(cliKeyLine('next', 'forkit-connect login'));
1525
2176
  return;
1526
2177
  }
1527
2178
  if (!status.device_paired) {
1528
- console.log('- note=Device identity is ready locally, but browser approval still needs to complete.');
1529
- console.log('- next=finish login approval on Forkit.dev, then rerun forkit-connect status');
2179
+ console.log(cliKeyLine('session', 'Local identity is ready, but browser approval still needs to finish.'));
2180
+ console.log(cliKeyLine('next', 'Finish approval in the browser, then rerun forkit-connect status'));
1530
2181
  return;
1531
2182
  }
1532
2183
  if (status.daemon_status === 'stopped') {
1533
- console.log('- note=Background sync is idle right now. This is expected until you start the local daemon.');
1534
- console.log('- next=run forkit-connect start');
2184
+ console.log(cliKeyLine('daemon', 'Background sync is idle until you start the local daemon.'));
2185
+ console.log(cliKeyLine('next', 'forkit-connect start'));
1535
2186
  return;
1536
2187
  }
1537
2188
  if (status.next_recommended_action) {
1538
- console.log(`- next_detail=${formatSmartInboxActionValue(status.next_recommended_action)}`);
2189
+ console.log(cliKeyLine('next', formatSmartInboxActionValue(status.next_recommended_action)));
1539
2190
  }
1540
2191
  }
2192
+ function buildRuntimeInteractiveDetailLines(runtime) {
2193
+ return [
2194
+ formatCliDetailLine('State', runtime.status.replaceAll('_', ' ')),
2195
+ formatCliDetailLine('Health', runtime.health_status),
2196
+ formatCliDetailLine('Endpoint', runtime.source_endpoint_label || runtime.runtime_type),
2197
+ formatCliDetailLine('Models', runtime.linked_models_count),
2198
+ formatCliDetailLine('Agents', runtime.linked_agents_count),
2199
+ formatCliDetailLine('Next', formatRuntimeRecommendedActionLabel(runtime.recommended_action)),
2200
+ ];
2201
+ }
2202
+ async function maybeShowInteractiveRuntimeReviewTable(summary) {
2203
+ const healthy = summary.runtimes.filter((runtime) => runtime.status !== 'unavailable' && runtime.health_status === 'detected');
2204
+ const review = summary.runtimes.filter((runtime) => !healthy.includes(runtime));
2205
+ return await viewInteractiveTable({
2206
+ title: '[forkit-connect] Runtime Review',
2207
+ subtitle: `${formatCliCompactCount(summary.total_runtimes, 'runtime')} on this device`,
2208
+ sections: [
2209
+ { id: 'healthy', label: 'Healthy', tone: 'good', rows: healthy },
2210
+ { id: 'review', label: 'Needs review', tone: 'warm', rows: review },
2211
+ ],
2212
+ columns: [
2213
+ { header: 'Runtime', width: 24, render: (runtime) => runtime.runtime_name },
2214
+ { header: 'State', width: 18, render: (runtime) => runtime.status.replaceAll('_', ' ') },
2215
+ { header: 'Health', width: 12, render: (runtime) => runtime.health_status },
2216
+ { header: 'Next', width: 24, render: (runtime) => formatRuntimeRecommendedActionLabel(runtime.recommended_action) },
2217
+ ],
2218
+ detailLines: buildRuntimeInteractiveDetailLines,
2219
+ detailTitle: () => 'RUNTIME SNAPSHOT',
2220
+ emptyState: 'No runtimes are available right now.',
2221
+ });
2222
+ }
1541
2223
  async function checkBackendSessionState(service) {
1542
2224
  const sessionRefValue = String(service.readSessionRef() || '').trim();
1543
2225
  if (!sessionRefValue)
@@ -1676,6 +2358,180 @@ async function promptSelection(label, options) {
1676
2358
  node_process_1.stdin.on('keypress', onKeypress);
1677
2359
  });
1678
2360
  }
2361
+ function paintCliTone(text, tone) {
2362
+ switch (tone) {
2363
+ case 'accent':
2364
+ return cliAccent(text);
2365
+ case 'good':
2366
+ return cliGood(text);
2367
+ case 'warm':
2368
+ return cliWarm(text);
2369
+ case 'danger':
2370
+ return cliDanger(text);
2371
+ default:
2372
+ return cliDim(text);
2373
+ }
2374
+ }
2375
+ function fitCliTableCell(value, width, align = 'left') {
2376
+ const rendered = truncateCliText(value, width);
2377
+ const pad = Math.max(0, width - cliPlainWidth(rendered));
2378
+ return align === 'right'
2379
+ ? `${' '.repeat(pad)}${rendered}`
2380
+ : `${rendered}${' '.repeat(pad)}`;
2381
+ }
2382
+ function buildCliTableBorder(widths, left, middle, right) {
2383
+ return `${left}${widths.map((width) => '─'.repeat(width + 2)).join(middle)}${right}`;
2384
+ }
2385
+ function buildCliTableRow(cells) {
2386
+ return `│${cells.map((cell) => ` ${fitCliTableCell(cell.value, cell.width, cell.align)} `).join('│')}│`;
2387
+ }
2388
+ function buildCliWideBox(title, lines, width) {
2389
+ const innerWidth = Math.max(24, width);
2390
+ const safeTitle = truncateCliText(title, Math.max(8, innerWidth - 4));
2391
+ const topPad = Math.max(0, innerWidth - cliPlainWidth(safeTitle) - 2);
2392
+ const top = `┌─ ${safeTitle} ${'─'.repeat(topPad)}┐`;
2393
+ const body = lines.map((line) => `│ ${fitCliTableCell(line, innerWidth)} │`);
2394
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
2395
+ return [top, ...body, bottom];
2396
+ }
2397
+ function cliTableCell(value, width, align) {
2398
+ return align ? { value, width, align } : { value, width };
2399
+ }
2400
+ function formatCliTableTabs(sections, activeIndex) {
2401
+ return sections
2402
+ .map((section, index) => {
2403
+ const label = `${section.label} ${section.rows.length}`;
2404
+ if (index === activeIndex) {
2405
+ return paintCli(` ${label} `, '7');
2406
+ }
2407
+ return paintCliTone(`[${label}]`, section.tone ?? 'muted');
2408
+ })
2409
+ .join(' ');
2410
+ }
2411
+ async function viewInteractiveTable(options) {
2412
+ const sections = options.sections.filter((section) => section.rows.length > 0);
2413
+ if (!sections.length || !node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY || hasFlag('--plain')) {
2414
+ return false;
2415
+ }
2416
+ (0, node_readline_1.emitKeypressEvents)(node_process_1.stdin);
2417
+ const canUseRawMode = typeof node_process_1.stdin.setRawMode === 'function';
2418
+ const previousRawMode = canUseRawMode ? node_process_1.stdin.isRaw : false;
2419
+ if (canUseRawMode) {
2420
+ node_process_1.stdin.setRawMode(true);
2421
+ }
2422
+ let activeSectionIndex = 0;
2423
+ let selectedIndex = 0;
2424
+ let renderLineCount = 0;
2425
+ const visibleRows = 8;
2426
+ const reset = '\u001B[0m';
2427
+ const hideCursor = '\u001B[?25l';
2428
+ const showCursor = '\u001B[?25h';
2429
+ const inverse = '\u001B[7m';
2430
+ const render = () => {
2431
+ const activeSection = sections[activeSectionIndex] ?? sections[0];
2432
+ const rows = activeSection?.rows ?? [];
2433
+ const currentRow = rows[selectedIndex] ?? null;
2434
+ const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(visibleRows / 2), Math.max(0, rows.length - visibleRows)));
2435
+ const visibleSlice = rows.slice(startIndex, startIndex + visibleRows);
2436
+ const selectorWidth = 1;
2437
+ const columnWidths = [selectorWidth, ...options.columns.map((column) => column.width)];
2438
+ const headerLine = buildCliTableRow([
2439
+ cliTableCell(' ', selectorWidth),
2440
+ ...options.columns.map((column) => cliTableCell(column.header.toUpperCase(), column.width, column.align)),
2441
+ ]);
2442
+ const rowLines = visibleSlice.length > 0
2443
+ ? visibleSlice.map((row, offset) => {
2444
+ const absoluteIndex = startIndex + offset;
2445
+ const active = absoluteIndex === selectedIndex;
2446
+ const line = buildCliTableRow([
2447
+ cliTableCell(active ? cliAccent('›') : ' ', selectorWidth),
2448
+ ...options.columns.map((column, index) => cliTableCell(active && index === 0 ? cliBold(column.render(row)) : column.render(row), column.width, column.align)),
2449
+ ]);
2450
+ return active ? cliAccent(line) : line;
2451
+ })
2452
+ : buildCliWideBox('EMPTY', [options.emptyState], columnWidths.reduce((sum, value) => sum + value + 3, -1));
2453
+ const detail = currentRow ? options.detailLines(currentRow) : [options.emptyState];
2454
+ const innerTableWidth = columnWidths.reduce((sum, value) => sum + value + 3, -1);
2455
+ const tableTop = buildCliTableBorder(columnWidths, '┌', '┬', '┐');
2456
+ const tableMiddle = buildCliTableBorder(columnWidths, '├', '┼', '┤');
2457
+ const tableBottom = buildCliTableBorder(columnWidths, '└', '┴', '┘');
2458
+ const detailTitle = currentRow && options.detailTitle ? options.detailTitle(currentRow) : 'SELECTED ITEM';
2459
+ const detailBox = buildCliWideBox(detailTitle, detail, innerTableWidth);
2460
+ const lines = [
2461
+ cliBold(options.title),
2462
+ ...(options.subtitle ? [cliDim(options.subtitle)] : []),
2463
+ cliRule(),
2464
+ formatCliTableTabs(sections, activeSectionIndex),
2465
+ '',
2466
+ cliDim(tableTop),
2467
+ cliDim(headerLine),
2468
+ cliDim(tableMiddle),
2469
+ ...rowLines,
2470
+ cliDim(tableBottom),
2471
+ '',
2472
+ ...detailBox.map((line) => cliDim(line)),
2473
+ '',
2474
+ cliDim('Tab switch lane · Up/Down move · q or Esc exit'),
2475
+ ];
2476
+ if (renderLineCount > 0) {
2477
+ node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
2478
+ node_process_1.stdout.write('\u001B[0J');
2479
+ }
2480
+ node_process_1.stdout.write(`${hideCursor}${lines.join('\n')}\n`);
2481
+ renderLineCount = lines.length;
2482
+ };
2483
+ const clampSelection = () => {
2484
+ const activeRows = sections[activeSectionIndex]?.rows ?? [];
2485
+ selectedIndex = Math.max(0, Math.min(selectedIndex, Math.max(activeRows.length - 1, 0)));
2486
+ };
2487
+ render();
2488
+ await new Promise((resolve) => setTimeout(resolve, 40));
2489
+ await new Promise((resolve) => {
2490
+ const cleanup = () => {
2491
+ node_process_1.stdin.off('keypress', onKeypress);
2492
+ if (canUseRawMode) {
2493
+ node_process_1.stdin.setRawMode(previousRawMode === true);
2494
+ }
2495
+ node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
2496
+ node_process_1.stdout.write(`\u001B[0J${showCursor}`);
2497
+ resolve();
2498
+ };
2499
+ const onKeypress = (_value, key) => {
2500
+ if (key.ctrl && key.name === 'c') {
2501
+ cleanup();
2502
+ return;
2503
+ }
2504
+ if (key.name === 'up' || key.name === 'k') {
2505
+ selectedIndex = Math.max(0, selectedIndex - 1);
2506
+ render();
2507
+ return;
2508
+ }
2509
+ if (key.name === 'down' || key.name === 'j') {
2510
+ const rows = sections[activeSectionIndex]?.rows ?? [];
2511
+ selectedIndex = Math.min(Math.max(rows.length - 1, 0), selectedIndex + 1);
2512
+ render();
2513
+ return;
2514
+ }
2515
+ if (key.name === 'tab' || key.name === 'right' || key.name === 'l') {
2516
+ activeSectionIndex = (activeSectionIndex + 1) % sections.length;
2517
+ clampSelection();
2518
+ render();
2519
+ return;
2520
+ }
2521
+ if (key.name === 'left' || key.name === 'h') {
2522
+ activeSectionIndex = (activeSectionIndex - 1 + sections.length) % sections.length;
2523
+ clampSelection();
2524
+ render();
2525
+ return;
2526
+ }
2527
+ if (key.name === 'escape' || key.name === 'q' || key.name === 'return' || key.name === 'enter') {
2528
+ cleanup();
2529
+ }
2530
+ };
2531
+ node_process_1.stdin.on('keypress', onKeypress);
2532
+ });
2533
+ return true;
2534
+ }
1679
2535
  async function promptText(question, options) {
1680
2536
  const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
1681
2537
  try {
@@ -1810,9 +2666,9 @@ async function run() {
1810
2666
  return;
1811
2667
  }
1812
2668
  printPublicStatusOverview(displayOverview);
1813
- console.log(`- secure storage=${secureStorage.backend}`);
2669
+ console.log(cliKeyLine('secure', secureStorage.backend));
1814
2670
  if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
1815
- console.log(`- secure storage detail=${secureStorage.detail}`);
2671
+ console.log(cliKeyLine('secure note', secureStorage.detail));
1816
2672
  }
1817
2673
  printPublicStatusGuidance(displayOverview, sessionState);
1818
2674
  };
@@ -1828,17 +2684,16 @@ async function run() {
1828
2684
  return;
1829
2685
  }
1830
2686
  const freshness = inbox.summary.freshness_state ?? 'fresh';
1831
- const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
1832
- ? Number(inbox.summary.snapshot_age_seconds)
1833
- : 0;
1834
- console.log(`[forkit-connect] Inbox snapshot freshness=${freshness} age_seconds=${ageSeconds}`);
2687
+ if (await maybeShowInteractiveInboxTable(inbox)) {
2688
+ return;
2689
+ }
2690
+ printSmartInbox(inbox);
1835
2691
  if (freshness === 'stale') {
1836
- console.log('[forkit-connect] Snapshot is stale. Run `forkit-connect inbox --refresh` for immediate reconcile.');
2692
+ console.log(cliKeyLine('refresh', 'Run forkit-connect inbox --refresh for an immediate reconcile.'));
1837
2693
  }
1838
2694
  else if (freshness === 'syncing') {
1839
- console.log('[forkit-connect] Background reconcile is running; results will self-refresh after completion.');
2695
+ console.log(cliKeyLine('refresh', 'Background reconcile is still running.'));
1840
2696
  }
1841
- printSmartInbox(inbox);
1842
2697
  };
1843
2698
  const runPublicCollectedChanges = () => {
1844
2699
  const rawLimit = limitArg !== null ? Number(limitArg) : 20;
@@ -4113,16 +4968,13 @@ async function run() {
4113
4968
  return;
4114
4969
  }
4115
4970
  const accountLimits = await loadCliAccountLimits().catch(() => null);
4116
- console.log(`[forkit-connect] Workspaces: ${workspaces.length}`);
4117
- if (accountLimits) {
4118
- console.log(`- welcome=${accountLimits.displayName || 'there'}`);
4119
- console.log(`- plan=${accountLimits.planName}`);
4120
- console.log(`- workspaces_left=${formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')}`);
4121
- console.log(`- projects_left=${formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')}`);
4122
- }
4123
- for (const workspace of workspaces) {
4124
- console.log(formatWorkspaceAccessLine(workspace));
4971
+ if (await maybeShowInteractiveWorkspaceTable(workspaces)) {
4972
+ return;
4125
4973
  }
4974
+ printWorkspaceListSurface(workspaces, {
4975
+ displayName: accountLimits?.displayName || null,
4976
+ accountLimits,
4977
+ });
4126
4978
  return;
4127
4979
  }
4128
4980
  if (subcommand === 'select') {
@@ -4173,6 +5025,9 @@ async function run() {
4173
5025
  printJson(summary);
4174
5026
  return;
4175
5027
  }
5028
+ if (await maybeShowInteractiveRuntimeReviewTable(summary)) {
5029
+ return;
5030
+ }
4176
5031
  printRuntimeReview(summary);
4177
5032
  return;
4178
5033
  }
@@ -4426,10 +5281,17 @@ async function run() {
4426
5281
  if (command === 'review') {
4427
5282
  const reviewScan = await service.scanRuntime();
4428
5283
  const snapshot = service.buildReviewSnapshot(reviewScan.summary);
5284
+ if (hasFlag('--json')) {
5285
+ printJson(snapshot);
5286
+ return;
5287
+ }
4429
5288
  if (snapshot.total === 0) {
4430
5289
  console.log('No review items found. Run `forkit-connect scan` first or ensure local runtimes are available.');
4431
5290
  return;
4432
5291
  }
5292
+ if (await maybeShowInteractiveDiscoveryReviewTable(snapshot)) {
5293
+ return;
5294
+ }
4433
5295
  printReviewSnapshot(snapshot);
4434
5296
  return;
4435
5297
  }
@@ -4480,6 +5342,9 @@ async function run() {
4480
5342
  printJson(summary);
4481
5343
  return;
4482
5344
  }
5345
+ if (await maybeShowInteractiveRuntimeReviewTable(summary)) {
5346
+ return;
5347
+ }
4483
5348
  printRuntimeReview(summary);
4484
5349
  return;
4485
5350
  }
@@ -4920,10 +5785,17 @@ async function run() {
4920
5785
  }
4921
5786
  if (subcommand === 'review') {
4922
5787
  const snapshot = service.buildAgentReviewSnapshot();
5788
+ if (hasFlag('--json')) {
5789
+ printJson(snapshot);
5790
+ return;
5791
+ }
4923
5792
  if (snapshot.total === 0) {
4924
5793
  console.log('No agent items found. Run `forkit-connect agent scan` first.');
4925
5794
  return;
4926
5795
  }
5796
+ if (await maybeShowInteractiveAgentReviewTable(snapshot)) {
5797
+ return;
5798
+ }
4927
5799
  printAgentReview(snapshot);
4928
5800
  return;
4929
5801
  }
@@ -5319,22 +6191,14 @@ async function run() {
5319
6191
  : typeof tokenPayload?.role === 'string'
5320
6192
  ? tokenPayload.role
5321
6193
  : null;
5322
- if (email || platformRole) {
5323
- const identityParts = [
5324
- email ? `account=${email}` : null,
5325
- platformRole ? `platform_role=${platformRole}` : null,
5326
- ].filter(Boolean);
5327
- console.log(`[forkit-connect] ${identityParts.join(' | ')}`);
5328
- }
5329
6194
  const workspaces = Array.isArray(payload.workspaces) ? payload.workspaces : [];
5330
- console.log(`[forkit-connect] Workspaces: ${workspaces.length}`);
5331
- if (workspaces.length === 0) {
5332
- console.log('[forkit-connect] No accessible workspaces returned by /api/profiles/access.');
6195
+ if (await maybeShowInteractiveWorkspaceTable(workspaces)) {
5333
6196
  return;
5334
6197
  }
5335
- for (const workspace of workspaces) {
5336
- console.log(formatWorkspaceAccessLine(workspace));
5337
- }
6198
+ printWorkspaceListSurface(workspaces, {
6199
+ accountEmail: email,
6200
+ platformRole,
6201
+ });
5338
6202
  return;
5339
6203
  }
5340
6204
  if (command === 'projects') {
@@ -5373,17 +6237,11 @@ async function run() {
5373
6237
  process.exitCode = 2;
5374
6238
  return;
5375
6239
  }
5376
- console.log('Authenticated successfully.');
5377
6240
  const projects = Array.isArray(result.body.projects) ? result.body.projects : [];
5378
- console.log(`Projects: ${projects.length}`);
5379
- if (projects.length === 0) {
5380
- console.log('No projects found in this workspace.');
5381
- console.log('Run `forkit-connect workspace select --workspace <id> --project-name "<name>"` to create the first project here.');
6241
+ if (await maybeShowInteractiveProjectTable(projects, workspaceId)) {
5382
6242
  return;
5383
6243
  }
5384
- for (const project of projects) {
5385
- console.log(formatWorkspaceProjectLine(project));
5386
- }
6244
+ printProjectListSurface(projects, workspaceId);
5387
6245
  return;
5388
6246
  }
5389
6247
  if (command === 'bind') {