@neurcode-ai/cli 0.19.2 → 0.19.4

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 (43) hide show
  1. package/README.md +12 -1
  2. package/dist/commands/runtime-doctor.d.ts.map +1 -1
  3. package/dist/commands/runtime-doctor.js +53 -3
  4. package/dist/commands/runtime-doctor.js.map +1 -1
  5. package/dist/commands/runtime-sync.d.ts.map +1 -1
  6. package/dist/commands/runtime-sync.js +9 -5
  7. package/dist/commands/runtime-sync.js.map +1 -1
  8. package/dist/commands/runtime.d.ts.map +1 -1
  9. package/dist/commands/runtime.js +40 -0
  10. package/dist/commands/runtime.js.map +1 -1
  11. package/dist/commands/session-hook.d.ts.map +1 -1
  12. package/dist/commands/session-hook.js +125 -8
  13. package/dist/commands/session-hook.js.map +1 -1
  14. package/dist/commands/session.d.ts +13 -0
  15. package/dist/commands/session.d.ts.map +1 -1
  16. package/dist/commands/session.js +293 -128
  17. package/dist/commands/session.js.map +1 -1
  18. package/dist/index.js +8 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/runtime-build.json +5 -5
  21. package/dist/utils/profile-drift-recovery.d.ts +40 -0
  22. package/dist/utils/profile-drift-recovery.d.ts.map +1 -0
  23. package/dist/utils/profile-drift-recovery.js +235 -0
  24. package/dist/utils/profile-drift-recovery.js.map +1 -0
  25. package/dist/utils/runtime-companion.d.ts.map +1 -1
  26. package/dist/utils/runtime-companion.js +9 -1
  27. package/dist/utils/runtime-companion.js.map +1 -1
  28. package/dist/utils/runtime-live.d.ts.map +1 -1
  29. package/dist/utils/runtime-live.js +12 -3
  30. package/dist/utils/runtime-live.js.map +1 -1
  31. package/dist/utils/runtime-outbox.d.ts +21 -1
  32. package/dist/utils/runtime-outbox.d.ts.map +1 -1
  33. package/dist/utils/runtime-outbox.js +353 -14
  34. package/dist/utils/runtime-outbox.js.map +1 -1
  35. package/dist/utils/runtime-privacy.d.ts +11 -0
  36. package/dist/utils/runtime-privacy.d.ts.map +1 -0
  37. package/dist/utils/runtime-privacy.js +321 -0
  38. package/dist/utils/runtime-privacy.js.map +1 -0
  39. package/dist/utils/v0-governance.d.ts +12 -1
  40. package/dist/utils/v0-governance.d.ts.map +1 -1
  41. package/dist/utils/v0-governance.js +18 -4
  42. package/dist/utils/v0-governance.js.map +1 -1
  43. package/package.json +2 -2
@@ -62,6 +62,7 @@ exports.verifyAIChangeRecordForCli = verifyAIChangeRecordForCli;
62
62
  exports.structuralUnderstandingCommand = structuralUnderstandingCommand;
63
63
  exports.listSessionsCommand = listSessionsCommand;
64
64
  exports.endSessionCommand = endSessionCommand;
65
+ exports.endSessionCommandWithDependencies = endSessionCommandWithDependencies;
65
66
  exports.sessionStatusCommand = sessionStatusCommand;
66
67
  exports.listLocalSessionsCommand = listLocalSessionsCommand;
67
68
  exports.currentLocalSessionCommand = currentLocalSessionCommand;
@@ -86,6 +87,7 @@ const repo_brain_impact_1 = require("../utils/repo-brain-impact");
86
87
  const structural_understanding_1 = require("../utils/structural-understanding");
87
88
  const agent_guard_supervisor_1 = require("../utils/agent-guard-supervisor");
88
89
  const hook_heartbeat_1 = require("../utils/hook-heartbeat");
90
+ const profile_drift_recovery_1 = require("../utils/profile-drift-recovery");
89
91
  const node_child_process_1 = require("node:child_process");
90
92
  const node_fs_1 = require("node:fs");
91
93
  const node_path_1 = require("node:path");
@@ -459,6 +461,19 @@ function buildLocalGovernanceStatus(options = {}) {
459
461
  };
460
462
  }
461
463
  const recentEvents = session.events.slice(-10);
464
+ const staleness = (0, v0_governance_1.getProfileStaleness)(repoRoot);
465
+ const profileAction = (0, v0_governance_1.profileFreshnessActionForSession)(staleness, session.profileHash);
466
+ const pendingProfileDecisions = (0, profile_drift_recovery_1.pendingProfileDriftDecisions)(session);
467
+ const profileFreshness = (0, v0_governance_1.buildProfileFreshnessSignal)(staleness, profileAction, {
468
+ sessionProfileHash: session.profileHash,
469
+ ...(profileAction === 'session_restart_required'
470
+ ? {
471
+ recoveryReason: 'active_session_profile_changed',
472
+ recoveryCommand: profile_drift_recovery_1.PROFILE_DRIFT_RECOVERY_COMMAND,
473
+ unresolvedHumanDecisions: pendingProfileDecisions.length > 0,
474
+ }
475
+ : {}),
476
+ });
462
477
  const latestBlock = [...session.events].reverse().find((event) => event.type === 'check_block');
463
478
  const latestApprovalContext = approvalContextFrom(latestBlock);
464
479
  const suggestedApprovalPath = latestApprovalContext?.suggestedApprovalPath ||
@@ -472,6 +487,7 @@ function buildLocalGovernanceStatus(options = {}) {
472
487
  status: session.status,
473
488
  goal: session.contract.goal,
474
489
  profileHash: session.profileHash,
490
+ profileFreshness,
475
491
  scopeMode: session.contract.scopeMode,
476
492
  planCoherenceMode: session.contract.planCoherenceMode ?? 'warn',
477
493
  agentPlan: session.contract.agentPlan ?? null,
@@ -533,6 +549,14 @@ function localGovernanceStatusCommand(options = {}) {
533
549
  console.log(`Session: ${chalk.white(activeStatus.sessionId)} ${activeStatus.active ? chalk.green('active') : chalk.dim(activeStatus.status)}`);
534
550
  console.log(`Goal: ${chalk.white(truncate(activeStatus.goal))}`);
535
551
  console.log(`Scope: ${chalk.white(activeStatus.scopeMode)}`);
552
+ console.log(`Profile: ${chalk.white(activeStatus.profileFreshness.status)} cache · ` +
553
+ `${chalk.white(activeStatus.profileFreshness.sessionCompatibility)} session`);
554
+ if (activeStatus.profileFreshness.sessionCompatibility === 'incompatible') {
555
+ console.log(chalk.yellow(`Hashes: session ${activeStatus.profileHash.slice(0, 12)} · ` +
556
+ `current ${activeStatus.profileFreshness.currentProfileHash.slice(0, 12)}`));
557
+ console.log(chalk.yellow(`Recover: ${profile_drift_recovery_1.PROFILE_DRIFT_RECOVERY_COMMAND} ` +
558
+ `(--force abandons unresolved operator state${activeStatus.profileFreshness.unresolvedHumanDecisions ? '; unresolved decisions are present' : ''})`));
559
+ }
536
560
  console.log(`Plan: ${chalk.white(activeStatus.planCoherenceMode)}${activeStatus.agentPlanRevision ? chalk.dim(` · rev ${activeStatus.agentPlanRevision}`) : ''}`);
537
561
  console.log(`Agent: ${chalk.white(activeStatus.agentInvocation.status.replace(/_/g, ' '))}` +
538
562
  chalk.dim(` · score ${activeStatus.agentInvocation.score}`) +
@@ -1484,11 +1508,15 @@ function extractRecordAndReceipt(value) {
1484
1508
  function collectChangeRecordImpactPaths(record) {
1485
1509
  const facts = record?.accountability?.facts ?? {};
1486
1510
  const pathTokens = record?.intent?.contract?.target?.pathTokens ?? [];
1511
+ const intentSummaryPaths = record?.intent?.summary?.paths ?? [];
1512
+ const expectedPathGlobs = record?.intent?.expectedPathGlobs ?? [];
1487
1513
  const candidates = [
1488
1514
  ...(Array.isArray(facts.touchedPaths) ? facts.touchedPaths : []),
1489
1515
  ...(Array.isArray(facts.allowedPaths) ? facts.allowedPaths : []),
1490
1516
  ...(Array.isArray(facts.warnedPaths) ? facts.warnedPaths : []),
1491
1517
  ...(Array.isArray(pathTokens) ? pathTokens : []),
1518
+ ...(Array.isArray(intentSummaryPaths) ? intentSummaryPaths : []),
1519
+ ...(Array.isArray(expectedPathGlobs) ? expectedPathGlobs : []),
1492
1520
  ];
1493
1521
  return Array.from(new Set(candidates
1494
1522
  .filter((p) => typeof p === 'string' && p.trim().length > 0)
@@ -1954,157 +1982,294 @@ async function listSessionsCommand(options) {
1954
1982
  * End a session
1955
1983
  */
1956
1984
  async function endSessionCommand(options) {
1985
+ return endSessionCommandWithDependencies(options);
1986
+ }
1987
+ function endSessionOutput(options, payload, exitCode = 0) {
1988
+ if (options.json) {
1989
+ console.log(JSON.stringify({ ...payload, exitCode }, null, 2));
1990
+ }
1991
+ else if (payload.ok === true) {
1992
+ console.log(chalk.green(String(payload.message || 'Session ended.')));
1993
+ if (payload.sessionId)
1994
+ console.log(chalk.dim(`Session: ${payload.sessionId}`));
1995
+ if (payload.replayHash)
1996
+ console.log(chalk.dim(`replayHash: ${payload.replayHash}`));
1997
+ }
1998
+ else {
1999
+ console.error(chalk.red(String(payload.message || payload.error || 'Session end failed.')));
2000
+ const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
2001
+ for (const candidate of candidates) {
2002
+ const item = candidate;
2003
+ console.error(chalk.dim(` ${item.sessionId || 'unknown'}: ${item.command || ''}`));
2004
+ }
2005
+ }
2006
+ process.exitCode = exitCode;
2007
+ }
2008
+ async function finishLocalGovernanceSession(repoRoot, session) {
2009
+ if (session.status === 'finished') {
2010
+ return {
2011
+ ok: true,
2012
+ ended: false,
2013
+ mode: 'local',
2014
+ status: 'already_finished',
2015
+ sessionId: session.sessionId,
2016
+ replayHash: session.replayHash || null,
2017
+ message: `Local governance session ${session.sessionId} is already finished.`,
2018
+ };
2019
+ }
2020
+ (0, governance_runtime_1.appendEvent)(repoRoot, session.sessionId, {
2021
+ type: 'user_decision',
2022
+ ts: new Date().toISOString(),
2023
+ decision: 'local_session_end_requested',
2024
+ message: 'Operator ended the local governance session.',
2025
+ detail: {
2026
+ source: 'local_cli',
2027
+ command: 'session end',
2028
+ },
2029
+ });
2030
+ const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, {
2031
+ reason: 'local_session_end_requested',
2032
+ });
2033
+ if (!finished)
2034
+ throw new Error(`Local governance session ${session.sessionId} could not be finished.`);
2035
+ (0, agent_guard_supervisor_1.stopSupervisorOnSessionCompletion)(repoRoot);
2036
+ let liveStatusPublished = true;
1957
2037
  try {
1958
- const config = (0, config_1.loadConfig)();
1959
- if (!config.apiKey) {
1960
- config.apiKey = (0, config_1.requireApiKey)();
2038
+ await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, finished);
2039
+ }
2040
+ catch {
2041
+ liveStatusPublished = false;
2042
+ }
2043
+ const replay = (0, governance_runtime_1.replaySession)(finished);
2044
+ return {
2045
+ ok: true,
2046
+ ended: true,
2047
+ mode: 'local',
2048
+ status: finished.status,
2049
+ sessionId: finished.sessionId,
2050
+ replayHash: finished.replayHash,
2051
+ replayVerified: replay.matchesOriginal,
2052
+ liveStatusPublished,
2053
+ recordPath: `.neurcode/sessions/${finished.sessionId}.json`,
2054
+ evidencePath: (0, node_path_1.relative)(repoRoot, (0, governance_runtime_1.aiChangeRecordPath)(repoRoot, finished.sessionId)).replace(/\\/g, '/'),
2055
+ message: `Local governance session ${finished.sessionId} ended with replay-valid evidence.`,
2056
+ };
2057
+ }
2058
+ async function endSessionCommandWithDependencies(options, dependencies = {}) {
2059
+ const repoRoot = (0, v0_governance_1.resolveRepoRoot)(options.dir || process.cwd());
2060
+ const isInteractive = dependencies.isInteractive ??
2061
+ (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
2062
+ const prompt = dependencies.prompt ?? promptUser;
2063
+ try {
2064
+ if (options.sessionId) {
2065
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, options.sessionId);
2066
+ if (local) {
2067
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
2068
+ return;
2069
+ }
2070
+ if (options.local) {
2071
+ endSessionOutput(options, {
2072
+ ok: false,
2073
+ ended: false,
2074
+ mode: 'local',
2075
+ reason: 'local_session_not_found',
2076
+ sessionId: options.sessionId,
2077
+ message: `Local governance session ${options.sessionId} was not found.`,
2078
+ }, 2);
2079
+ return;
2080
+ }
1961
2081
  }
1962
- const client = new api_client_1.ApiClient(config);
1963
- let sessionId = options.sessionId;
1964
- // If no session ID provided, try to get from state
1965
- if (!sessionId) {
1966
- const stateSessionId = (0, state_1.getSessionId)();
1967
- sessionId = stateSessionId || undefined;
1968
- if (!sessionId) {
1969
- // List active sessions and let user choose
1970
- (0, messages_1.printInfo)('No Active Session', 'Looking for active sessions...');
1971
- const sessions = await client.getSessions(config.projectId, 10);
1972
- const activeSessions = sessions.filter(s => s.status === 'active');
1973
- if (activeSessions.length === 0) {
1974
- (0, messages_1.printInfo)('No Active Sessions', 'There are no active sessions to end.');
2082
+ else {
2083
+ const records = scanSessionRecords(repoRoot);
2084
+ if (records.active.length === 1) {
2085
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[0]));
2086
+ return;
2087
+ }
2088
+ if (records.active.length > 1) {
2089
+ if (!isInteractive()) {
2090
+ endSessionOutput(options, {
2091
+ ok: false,
2092
+ ended: false,
2093
+ mode: 'local',
2094
+ reason: 'multiple_local_sessions_noninteractive',
2095
+ candidates: records.active.map((session) => ({
2096
+ sessionId: session.sessionId,
2097
+ command: `neurcode session end --session-id ${session.sessionId}`,
2098
+ })),
2099
+ malformedRecords: records.malformed,
2100
+ message: 'Multiple active local governance sessions were found; noninteractive selection is disabled.',
2101
+ }, 2);
1975
2102
  return;
1976
2103
  }
1977
- if (activeSessions.length === 1) {
1978
- sessionId = activeSessions[0].sessionId;
1979
- const title = activeSessions[0].title || activeSessions[0].intentDescription || 'Untitled';
1980
- (0, messages_1.printInfo)('Found Active Session', `Ending: ${title}`);
1981
- }
1982
- else {
1983
- // Multiple active sessions - let user choose
1984
- (0, messages_1.printSection)('Multiple Active Sessions');
1985
- activeSessions.forEach((session, index) => {
1986
- const title = session.title || session.intentDescription || 'Untitled';
1987
- console.log(chalk.cyan(` ${index + 1}.`), chalk.white(title));
1988
- console.log(chalk.dim(` ${session.sessionId.substring(0, 20)}...`));
1989
- });
1990
- console.log('');
1991
- const answer = await promptUser(chalk.bold('Select session to end (1-' + activeSessions.length + '): '));
1992
- const choice = parseInt(answer, 10);
1993
- if (choice >= 1 && choice <= activeSessions.length) {
1994
- sessionId = activeSessions[choice - 1].sessionId;
1995
- }
1996
- else {
1997
- (0, messages_1.printError)('Invalid Selection', undefined, ['Please run the command again and select a valid number']);
1998
- process.exit(1);
1999
- }
2104
+ console.log(chalk.bold('Multiple active local governance sessions'));
2105
+ records.active.forEach((session, index) => {
2106
+ console.log(` ${index + 1}. ${session.sessionId} · ${truncate(session.contract.goal, 72)}`);
2107
+ });
2108
+ const answer = await prompt(`Select local session to end (1-${records.active.length}): `);
2109
+ const selected = Number.parseInt(answer, 10);
2110
+ if (!Number.isInteger(selected) || selected < 1 || selected > records.active.length) {
2111
+ endSessionOutput(options, {
2112
+ ok: false,
2113
+ ended: false,
2114
+ mode: 'local',
2115
+ reason: 'invalid_local_selection',
2116
+ message: 'No local session was ended.',
2117
+ }, 2);
2118
+ return;
2000
2119
  }
2120
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[selected - 1]));
2121
+ return;
2122
+ }
2123
+ if (options.local) {
2124
+ endSessionOutput(options, {
2125
+ ok: true,
2126
+ ended: false,
2127
+ mode: 'local',
2128
+ reason: 'no_active_local_session',
2129
+ malformedRecords: records.malformed,
2130
+ message: 'No active local governance session found.',
2131
+ });
2132
+ return;
2001
2133
  }
2002
2134
  }
2003
- if (!sessionId) {
2004
- (0, messages_1.printError)('No Session Specified', undefined, [
2005
- 'No session ID provided and no active session found',
2006
- 'Usage: neurcode session end [session-id]',
2007
- 'Or set a session: neurcode init'
2008
- ]);
2009
- process.exit(1);
2135
+ let config = null;
2136
+ let client = dependencies.cloudClient;
2137
+ if (!client) {
2138
+ config = (0, config_1.loadConfig)();
2139
+ if (!config.apiKey)
2140
+ config.apiKey = (0, config_1.requireApiKey)();
2141
+ client = new api_client_1.ApiClient(config);
2010
2142
  }
2011
- // Get session details first
2012
- try {
2013
- const sessionData = await client.getSession(sessionId);
2014
- const session = sessionData.session;
2015
- if (session.status === 'completed') {
2016
- (0, messages_1.printWarning)('Session Already Completed', `Session "${session.title || session.intentDescription || sessionId}" is already ended.`);
2143
+ let sessionId = options.sessionId;
2144
+ if (!sessionId) {
2145
+ const stateSessionId = (0, state_1.getSessionId)() || undefined;
2146
+ if (stateSessionId && (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId)) {
2147
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId);
2148
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
2017
2149
  return;
2018
2150
  }
2019
- if (session.status === 'cancelled') {
2020
- (0, messages_1.printWarning)('Session Already Cancelled', `Session "${session.title || session.intentDescription || sessionId}" was already cancelled.`);
2151
+ sessionId = stateSessionId;
2152
+ }
2153
+ if (!sessionId) {
2154
+ const sessions = await client.getSessions(options.projectId || config?.projectId, 20);
2155
+ const active = sessions.filter((session) => session.status === 'active');
2156
+ if (active.length === 0) {
2157
+ endSessionOutput(options, {
2158
+ ok: true,
2159
+ ended: false,
2160
+ mode: 'cloud',
2161
+ reason: 'no_active_cloud_session',
2162
+ message: 'No active local or cloud session found.',
2163
+ });
2021
2164
  return;
2022
2165
  }
2023
- // Show session summary
2024
- const title = session.title || session.intentDescription || 'Untitled Session';
2025
- const filesCount = sessionData.files?.length || 0;
2026
- (0, messages_1.printSection)('Session Summary');
2027
- console.log(chalk.white(` Title: ${title}`));
2028
- console.log(chalk.white(` Files Changed: ${filesCount}`));
2029
- console.log(chalk.dim(` Session ID: ${sessionId}`));
2030
- console.log('');
2031
- // Confirm before ending
2032
- const confirm = await promptUser(chalk.bold('End this session? (y/n): '));
2033
- if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
2034
- (0, messages_1.printInfo)('Cancelled', 'Session was not ended.');
2166
+ if (active.length > 1 && !isInteractive()) {
2167
+ endSessionOutput(options, {
2168
+ ok: false,
2169
+ ended: false,
2170
+ mode: 'cloud',
2171
+ reason: 'multiple_cloud_sessions_noninteractive',
2172
+ candidates: active.map((session) => ({
2173
+ sessionId: session.sessionId,
2174
+ command: `neurcode session end --session-id ${session.sessionId}`,
2175
+ })),
2176
+ message: 'Multiple active cloud sessions were found; noninteractive selection is disabled.',
2177
+ }, 2);
2035
2178
  return;
2036
2179
  }
2037
- await client.endSession(sessionId);
2038
- // Clear session ID from local state if it matches the ended session
2039
- try {
2040
- const currentSessionId = (0, state_1.getSessionId)();
2041
- if (currentSessionId === sessionId) {
2042
- const { clearSessionId } = await Promise.resolve().then(() => __importStar(require('../utils/state')));
2043
- clearSessionId();
2044
- }
2045
- }
2046
- catch {
2047
- // Non-critical - continue if state clearing fails
2180
+ if (active.length === 1) {
2181
+ sessionId = active[0].sessionId;
2048
2182
  }
2049
- const firstName = await (0, messages_1.getUserFirstName)();
2050
- await (0, messages_1.printSuccessBanner)('Session Completed', `Great work, ${firstName}! Your session has been marked as complete.`);
2051
- (0, messages_1.printSuccess)('Session Ended Successfully', `"${title}" is now marked as completed.\n View in dashboard: dashboard.neurcode.com`);
2052
- // Display Session ROI Summary
2053
- try {
2054
- // Fetch ROI summary from API
2055
- const apiUrl = config.apiUrl || process.env.NEURCODE_API_URL || 'https://api.neurcode.com';
2056
- const roiUrl = `${apiUrl}/api/v1/roi/summary?timeRange=7d`;
2057
- const roiResponse = await fetch(roiUrl, {
2058
- headers: {
2059
- 'Authorization': `Bearer ${config.apiKey}`,
2060
- 'Content-Type': 'application/json',
2061
- },
2062
- }).catch(() => null);
2063
- if (roiResponse && roiResponse.ok) {
2064
- const roiData = await roiResponse.json().catch(() => null);
2065
- if (roiData && roiData.totalCapitalSaved) {
2066
- const capitalSaved = typeof roiData.totalCapitalSaved === 'string'
2067
- ? parseFloat(roiData.totalCapitalSaved)
2068
- : roiData.totalCapitalSaved;
2069
- const formattedAmount = capitalSaved.toFixed(2);
2070
- const dashboardUrl = 'https://neurcode.com/dashboard';
2071
- console.log('');
2072
- console.log(chalk.cyan('📊'), chalk.bold.white('Current Session ROI:'), chalk.green.bold(`+$${formattedAmount}`));
2073
- console.log(chalk.dim(` View full report: ${dashboardUrl}`));
2074
- console.log('');
2075
- }
2183
+ else {
2184
+ active.forEach((session, index) => {
2185
+ const title = session.title || session.intentDescription || 'Untitled';
2186
+ console.log(` ${index + 1}. ${title} · ${session.sessionId}`);
2187
+ });
2188
+ const answer = await prompt(`Select cloud session to end (1-${active.length}): `);
2189
+ const selected = Number.parseInt(answer, 10);
2190
+ if (!Number.isInteger(selected) || selected < 1 || selected > active.length) {
2191
+ endSessionOutput(options, {
2192
+ ok: false,
2193
+ ended: false,
2194
+ mode: 'cloud',
2195
+ reason: 'invalid_cloud_selection',
2196
+ message: 'No cloud session was ended.',
2197
+ }, 2);
2198
+ return;
2076
2199
  }
2077
- }
2078
- catch {
2079
- // Silently fail - ROI summary is a nice-to-have
2200
+ sessionId = active[selected - 1].sessionId;
2080
2201
  }
2081
2202
  }
2082
- catch (error) {
2083
- if (error.message?.includes('not found') || error.message?.includes('404')) {
2084
- (0, messages_1.printError)('Session Not Found', error, [
2085
- `Session "${sessionId}" could not be found`,
2086
- 'List your sessions: neurcode session list',
2087
- 'Verify the session ID is correct'
2088
- ]);
2089
- }
2090
- else {
2091
- throw error;
2092
- }
2203
+ if (!sessionId) {
2204
+ endSessionOutput(options, {
2205
+ ok: false,
2206
+ ended: false,
2207
+ mode: 'cloud',
2208
+ reason: 'cloud_session_not_resolved',
2209
+ message: 'No cloud session could be resolved.',
2210
+ }, 2);
2211
+ return;
2093
2212
  }
2094
- }
2095
- catch (error) {
2096
- if (error instanceof Error) {
2097
- if (error.message.includes('401') || error.message.includes('403')) {
2098
- await (0, messages_1.printAuthError)(error);
2213
+ const sessionData = await client.getSession(sessionId);
2214
+ const session = sessionData.session;
2215
+ if (session.status === 'completed' || session.status === 'cancelled') {
2216
+ endSessionOutput(options, {
2217
+ ok: true,
2218
+ ended: false,
2219
+ mode: 'cloud',
2220
+ status: session.status,
2221
+ sessionId,
2222
+ message: `Cloud session ${sessionId} is already ${session.status}.`,
2223
+ });
2224
+ return;
2225
+ }
2226
+ if (isInteractive()) {
2227
+ const confirm = await prompt(`End cloud session ${sessionId}? (y/n): `);
2228
+ if (!['y', 'yes'].includes(confirm.toLowerCase())) {
2229
+ endSessionOutput(options, {
2230
+ ok: true,
2231
+ ended: false,
2232
+ mode: 'cloud',
2233
+ reason: 'operator_cancelled',
2234
+ sessionId,
2235
+ message: 'Cloud session was not ended.',
2236
+ });
2237
+ return;
2099
2238
  }
2100
- else {
2101
- (0, messages_1.printError)('Failed to End Session', error);
2239
+ }
2240
+ await client.endSession(sessionId);
2241
+ try {
2242
+ if ((0, state_1.getSessionId)() === sessionId) {
2243
+ const { clearSessionId } = await Promise.resolve().then(() => __importStar(require('../utils/state')));
2244
+ clearSessionId();
2102
2245
  }
2103
2246
  }
2104
- else {
2105
- (0, messages_1.printError)('Failed to End Session', String(error));
2247
+ catch {
2248
+ // Legacy local cloud pointer cleanup is best-effort.
2106
2249
  }
2107
- process.exit(1);
2250
+ endSessionOutput(options, {
2251
+ ok: true,
2252
+ ended: true,
2253
+ mode: 'cloud',
2254
+ status: 'completed',
2255
+ sessionId,
2256
+ message: `Cloud session ${sessionId} ended successfully.`,
2257
+ });
2258
+ }
2259
+ catch (error) {
2260
+ const message = error instanceof Error ? error.message : String(error);
2261
+ const notFound = /not found|404/i.test(message);
2262
+ endSessionOutput(options, {
2263
+ ok: false,
2264
+ ended: false,
2265
+ mode: 'unknown',
2266
+ reason: notFound ? 'session_not_found' : 'session_end_failed',
2267
+ sessionId: options.sessionId || null,
2268
+ error: message,
2269
+ message: notFound
2270
+ ? `Session ${options.sessionId || ''} was not found locally or in the cloud.`.trim()
2271
+ : `Failed to end session: ${message}`,
2272
+ }, notFound ? 2 : 1);
2108
2273
  }
2109
2274
  }
2110
2275
  /**