@neurcode-ai/cli 0.19.2 → 0.19.3

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.
@@ -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}`) +
@@ -1954,157 +1978,294 @@ async function listSessionsCommand(options) {
1954
1978
  * End a session
1955
1979
  */
1956
1980
  async function endSessionCommand(options) {
1981
+ return endSessionCommandWithDependencies(options);
1982
+ }
1983
+ function endSessionOutput(options, payload, exitCode = 0) {
1984
+ if (options.json) {
1985
+ console.log(JSON.stringify({ ...payload, exitCode }, null, 2));
1986
+ }
1987
+ else if (payload.ok === true) {
1988
+ console.log(chalk.green(String(payload.message || 'Session ended.')));
1989
+ if (payload.sessionId)
1990
+ console.log(chalk.dim(`Session: ${payload.sessionId}`));
1991
+ if (payload.replayHash)
1992
+ console.log(chalk.dim(`replayHash: ${payload.replayHash}`));
1993
+ }
1994
+ else {
1995
+ console.error(chalk.red(String(payload.message || payload.error || 'Session end failed.')));
1996
+ const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
1997
+ for (const candidate of candidates) {
1998
+ const item = candidate;
1999
+ console.error(chalk.dim(` ${item.sessionId || 'unknown'}: ${item.command || ''}`));
2000
+ }
2001
+ }
2002
+ process.exitCode = exitCode;
2003
+ }
2004
+ async function finishLocalGovernanceSession(repoRoot, session) {
2005
+ if (session.status === 'finished') {
2006
+ return {
2007
+ ok: true,
2008
+ ended: false,
2009
+ mode: 'local',
2010
+ status: 'already_finished',
2011
+ sessionId: session.sessionId,
2012
+ replayHash: session.replayHash || null,
2013
+ message: `Local governance session ${session.sessionId} is already finished.`,
2014
+ };
2015
+ }
2016
+ (0, governance_runtime_1.appendEvent)(repoRoot, session.sessionId, {
2017
+ type: 'user_decision',
2018
+ ts: new Date().toISOString(),
2019
+ decision: 'local_session_end_requested',
2020
+ message: 'Operator ended the local governance session.',
2021
+ detail: {
2022
+ source: 'local_cli',
2023
+ command: 'session end',
2024
+ },
2025
+ });
2026
+ const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, {
2027
+ reason: 'local_session_end_requested',
2028
+ });
2029
+ if (!finished)
2030
+ throw new Error(`Local governance session ${session.sessionId} could not be finished.`);
2031
+ (0, agent_guard_supervisor_1.stopSupervisorOnSessionCompletion)(repoRoot);
2032
+ let liveStatusPublished = true;
1957
2033
  try {
1958
- const config = (0, config_1.loadConfig)();
1959
- if (!config.apiKey) {
1960
- config.apiKey = (0, config_1.requireApiKey)();
2034
+ await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, finished);
2035
+ }
2036
+ catch {
2037
+ liveStatusPublished = false;
2038
+ }
2039
+ const replay = (0, governance_runtime_1.replaySession)(finished);
2040
+ return {
2041
+ ok: true,
2042
+ ended: true,
2043
+ mode: 'local',
2044
+ status: finished.status,
2045
+ sessionId: finished.sessionId,
2046
+ replayHash: finished.replayHash,
2047
+ replayVerified: replay.matchesOriginal,
2048
+ liveStatusPublished,
2049
+ recordPath: `.neurcode/sessions/${finished.sessionId}.json`,
2050
+ evidencePath: (0, node_path_1.relative)(repoRoot, (0, governance_runtime_1.aiChangeRecordPath)(repoRoot, finished.sessionId)).replace(/\\/g, '/'),
2051
+ message: `Local governance session ${finished.sessionId} ended with replay-valid evidence.`,
2052
+ };
2053
+ }
2054
+ async function endSessionCommandWithDependencies(options, dependencies = {}) {
2055
+ const repoRoot = (0, v0_governance_1.resolveRepoRoot)(options.dir || process.cwd());
2056
+ const isInteractive = dependencies.isInteractive ??
2057
+ (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
2058
+ const prompt = dependencies.prompt ?? promptUser;
2059
+ try {
2060
+ if (options.sessionId) {
2061
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, options.sessionId);
2062
+ if (local) {
2063
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
2064
+ return;
2065
+ }
2066
+ if (options.local) {
2067
+ endSessionOutput(options, {
2068
+ ok: false,
2069
+ ended: false,
2070
+ mode: 'local',
2071
+ reason: 'local_session_not_found',
2072
+ sessionId: options.sessionId,
2073
+ message: `Local governance session ${options.sessionId} was not found.`,
2074
+ }, 2);
2075
+ return;
2076
+ }
1961
2077
  }
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.');
2078
+ else {
2079
+ const records = scanSessionRecords(repoRoot);
2080
+ if (records.active.length === 1) {
2081
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[0]));
2082
+ return;
2083
+ }
2084
+ if (records.active.length > 1) {
2085
+ if (!isInteractive()) {
2086
+ endSessionOutput(options, {
2087
+ ok: false,
2088
+ ended: false,
2089
+ mode: 'local',
2090
+ reason: 'multiple_local_sessions_noninteractive',
2091
+ candidates: records.active.map((session) => ({
2092
+ sessionId: session.sessionId,
2093
+ command: `neurcode session end --session-id ${session.sessionId}`,
2094
+ })),
2095
+ malformedRecords: records.malformed,
2096
+ message: 'Multiple active local governance sessions were found; noninteractive selection is disabled.',
2097
+ }, 2);
1975
2098
  return;
1976
2099
  }
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
- }
2100
+ console.log(chalk.bold('Multiple active local governance sessions'));
2101
+ records.active.forEach((session, index) => {
2102
+ console.log(` ${index + 1}. ${session.sessionId} · ${truncate(session.contract.goal, 72)}`);
2103
+ });
2104
+ const answer = await prompt(`Select local session to end (1-${records.active.length}): `);
2105
+ const selected = Number.parseInt(answer, 10);
2106
+ if (!Number.isInteger(selected) || selected < 1 || selected > records.active.length) {
2107
+ endSessionOutput(options, {
2108
+ ok: false,
2109
+ ended: false,
2110
+ mode: 'local',
2111
+ reason: 'invalid_local_selection',
2112
+ message: 'No local session was ended.',
2113
+ }, 2);
2114
+ return;
2000
2115
  }
2116
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[selected - 1]));
2117
+ return;
2118
+ }
2119
+ if (options.local) {
2120
+ endSessionOutput(options, {
2121
+ ok: true,
2122
+ ended: false,
2123
+ mode: 'local',
2124
+ reason: 'no_active_local_session',
2125
+ malformedRecords: records.malformed,
2126
+ message: 'No active local governance session found.',
2127
+ });
2128
+ return;
2001
2129
  }
2002
2130
  }
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);
2131
+ let config = null;
2132
+ let client = dependencies.cloudClient;
2133
+ if (!client) {
2134
+ config = (0, config_1.loadConfig)();
2135
+ if (!config.apiKey)
2136
+ config.apiKey = (0, config_1.requireApiKey)();
2137
+ client = new api_client_1.ApiClient(config);
2010
2138
  }
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.`);
2139
+ let sessionId = options.sessionId;
2140
+ if (!sessionId) {
2141
+ const stateSessionId = (0, state_1.getSessionId)() || undefined;
2142
+ if (stateSessionId && (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId)) {
2143
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId);
2144
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
2017
2145
  return;
2018
2146
  }
2019
- if (session.status === 'cancelled') {
2020
- (0, messages_1.printWarning)('Session Already Cancelled', `Session "${session.title || session.intentDescription || sessionId}" was already cancelled.`);
2147
+ sessionId = stateSessionId;
2148
+ }
2149
+ if (!sessionId) {
2150
+ const sessions = await client.getSessions(options.projectId || config?.projectId, 20);
2151
+ const active = sessions.filter((session) => session.status === 'active');
2152
+ if (active.length === 0) {
2153
+ endSessionOutput(options, {
2154
+ ok: true,
2155
+ ended: false,
2156
+ mode: 'cloud',
2157
+ reason: 'no_active_cloud_session',
2158
+ message: 'No active local or cloud session found.',
2159
+ });
2021
2160
  return;
2022
2161
  }
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.');
2162
+ if (active.length > 1 && !isInteractive()) {
2163
+ endSessionOutput(options, {
2164
+ ok: false,
2165
+ ended: false,
2166
+ mode: 'cloud',
2167
+ reason: 'multiple_cloud_sessions_noninteractive',
2168
+ candidates: active.map((session) => ({
2169
+ sessionId: session.sessionId,
2170
+ command: `neurcode session end --session-id ${session.sessionId}`,
2171
+ })),
2172
+ message: 'Multiple active cloud sessions were found; noninteractive selection is disabled.',
2173
+ }, 2);
2035
2174
  return;
2036
2175
  }
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
2176
+ if (active.length === 1) {
2177
+ sessionId = active[0].sessionId;
2048
2178
  }
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
- }
2179
+ else {
2180
+ active.forEach((session, index) => {
2181
+ const title = session.title || session.intentDescription || 'Untitled';
2182
+ console.log(` ${index + 1}. ${title} · ${session.sessionId}`);
2183
+ });
2184
+ const answer = await prompt(`Select cloud session to end (1-${active.length}): `);
2185
+ const selected = Number.parseInt(answer, 10);
2186
+ if (!Number.isInteger(selected) || selected < 1 || selected > active.length) {
2187
+ endSessionOutput(options, {
2188
+ ok: false,
2189
+ ended: false,
2190
+ mode: 'cloud',
2191
+ reason: 'invalid_cloud_selection',
2192
+ message: 'No cloud session was ended.',
2193
+ }, 2);
2194
+ return;
2076
2195
  }
2077
- }
2078
- catch {
2079
- // Silently fail - ROI summary is a nice-to-have
2196
+ sessionId = active[selected - 1].sessionId;
2080
2197
  }
2081
2198
  }
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
- }
2199
+ if (!sessionId) {
2200
+ endSessionOutput(options, {
2201
+ ok: false,
2202
+ ended: false,
2203
+ mode: 'cloud',
2204
+ reason: 'cloud_session_not_resolved',
2205
+ message: 'No cloud session could be resolved.',
2206
+ }, 2);
2207
+ return;
2093
2208
  }
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);
2209
+ const sessionData = await client.getSession(sessionId);
2210
+ const session = sessionData.session;
2211
+ if (session.status === 'completed' || session.status === 'cancelled') {
2212
+ endSessionOutput(options, {
2213
+ ok: true,
2214
+ ended: false,
2215
+ mode: 'cloud',
2216
+ status: session.status,
2217
+ sessionId,
2218
+ message: `Cloud session ${sessionId} is already ${session.status}.`,
2219
+ });
2220
+ return;
2221
+ }
2222
+ if (isInteractive()) {
2223
+ const confirm = await prompt(`End cloud session ${sessionId}? (y/n): `);
2224
+ if (!['y', 'yes'].includes(confirm.toLowerCase())) {
2225
+ endSessionOutput(options, {
2226
+ ok: true,
2227
+ ended: false,
2228
+ mode: 'cloud',
2229
+ reason: 'operator_cancelled',
2230
+ sessionId,
2231
+ message: 'Cloud session was not ended.',
2232
+ });
2233
+ return;
2099
2234
  }
2100
- else {
2101
- (0, messages_1.printError)('Failed to End Session', error);
2235
+ }
2236
+ await client.endSession(sessionId);
2237
+ try {
2238
+ if ((0, state_1.getSessionId)() === sessionId) {
2239
+ const { clearSessionId } = await Promise.resolve().then(() => __importStar(require('../utils/state')));
2240
+ clearSessionId();
2102
2241
  }
2103
2242
  }
2104
- else {
2105
- (0, messages_1.printError)('Failed to End Session', String(error));
2243
+ catch {
2244
+ // Legacy local cloud pointer cleanup is best-effort.
2106
2245
  }
2107
- process.exit(1);
2246
+ endSessionOutput(options, {
2247
+ ok: true,
2248
+ ended: true,
2249
+ mode: 'cloud',
2250
+ status: 'completed',
2251
+ sessionId,
2252
+ message: `Cloud session ${sessionId} ended successfully.`,
2253
+ });
2254
+ }
2255
+ catch (error) {
2256
+ const message = error instanceof Error ? error.message : String(error);
2257
+ const notFound = /not found|404/i.test(message);
2258
+ endSessionOutput(options, {
2259
+ ok: false,
2260
+ ended: false,
2261
+ mode: 'unknown',
2262
+ reason: notFound ? 'session_not_found' : 'session_end_failed',
2263
+ sessionId: options.sessionId || null,
2264
+ error: message,
2265
+ message: notFound
2266
+ ? `Session ${options.sessionId || ''} was not found locally or in the cloud.`.trim()
2267
+ : `Failed to end session: ${message}`,
2268
+ }, notFound ? 2 : 1);
2108
2269
  }
2109
2270
  }
2110
2271
  /**