@fnclaude/cli 0.7.3 → 0.7.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.
- package/package.json +1 -1
- package/src/mcp/socketListener.ts +15 -1
- package/src/sessionState.ts +125 -1
package/package.json
CHANGED
|
@@ -17,7 +17,10 @@ import { createServer, type Server, type Socket } from 'node:net';
|
|
|
17
17
|
import { writeFile, unlink } from 'node:fs/promises';
|
|
18
18
|
import type { Config } from '../config.js';
|
|
19
19
|
import { handoffContentPath, type HandoffSpec } from '../handoff.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
appendRestartReminder,
|
|
22
|
+
readLivePermissionMode,
|
|
23
|
+
} from '../sessionState.js';
|
|
21
24
|
import {
|
|
22
25
|
encodeResponse,
|
|
23
26
|
type CopyRequest,
|
|
@@ -308,6 +311,17 @@ export class SocketListener {
|
|
|
308
311
|
}
|
|
309
312
|
const { magic, rest } = splitLeadingMagic(withOverrides);
|
|
310
313
|
|
|
314
|
+
// Append a system-reminder to the session JSONL so the resumed model
|
|
315
|
+
// sees a fresh directive to continue the in-flight work rather than
|
|
316
|
+
// treat the restart as a hard reset and idle (issue #77).
|
|
317
|
+
appendRestartReminder(this.launchCWD, sid, {
|
|
318
|
+
model: req.model,
|
|
319
|
+
effort: req.effort,
|
|
320
|
+
permissionMode: req.permission_mode,
|
|
321
|
+
agent: req.agent,
|
|
322
|
+
ide: req.ide === true,
|
|
323
|
+
});
|
|
324
|
+
|
|
311
325
|
const argv = [...magic, this.launchCWD, '--resume', sid, ...rest];
|
|
312
326
|
this.stashArgv(argv);
|
|
313
327
|
return { action: 'done' };
|
package/src/sessionState.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// CWD encoding for Claude Code's project dir naming scheme, and JSONL
|
|
4
4
|
// permission-mode last-wins scan over a session log.
|
|
5
5
|
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { appendFileSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
9
|
import { join } from 'node:path';
|
|
9
10
|
|
|
@@ -92,3 +93,126 @@ export function readLivePermissionMode(
|
|
|
92
93
|
}
|
|
93
94
|
return latest;
|
|
94
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Overrides that may have landed alongside the restart. When any of these
|
|
99
|
+
* are set the appended reminder names them so the resumed model can briefly
|
|
100
|
+
* acknowledge the change before continuing the pre-restart work.
|
|
101
|
+
*/
|
|
102
|
+
export interface RestartReminderOverrides {
|
|
103
|
+
model?: string;
|
|
104
|
+
effort?: string;
|
|
105
|
+
permissionMode?: string;
|
|
106
|
+
agent?: string;
|
|
107
|
+
/** True iff `--ide` is being added on the relaunch. */
|
|
108
|
+
ide?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read the trailing entries of `data` and return the most recent `uuid`
|
|
113
|
+
* field, or `null` if none found. Used to link the appended reminder into
|
|
114
|
+
* the JSONL parent-chain.
|
|
115
|
+
*/
|
|
116
|
+
function lastEntryUUID(data: string): string | null {
|
|
117
|
+
const lines = data.split('\n');
|
|
118
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
if (!line || line.length === 0) continue;
|
|
121
|
+
let parsed: { uuid?: unknown };
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(line) as typeof parsed;
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof parsed.uuid === 'string' && parsed.uuid.length > 0) {
|
|
128
|
+
return parsed.uuid;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Render the system-reminder body text, optionally naming overrides. */
|
|
135
|
+
export function renderRestartReminderContent(
|
|
136
|
+
overrides?: RestartReminderOverrides,
|
|
137
|
+
): string {
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
if (overrides?.model && overrides.model !== '') {
|
|
140
|
+
parts.push(`model swap to ${overrides.model}`);
|
|
141
|
+
}
|
|
142
|
+
if (overrides?.effort && overrides.effort !== '') {
|
|
143
|
+
parts.push(`effort=${overrides.effort}`);
|
|
144
|
+
}
|
|
145
|
+
if (overrides?.permissionMode && overrides.permissionMode !== '') {
|
|
146
|
+
parts.push(`permission-mode=${overrides.permissionMode}`);
|
|
147
|
+
}
|
|
148
|
+
if (overrides?.agent && overrides.agent !== '') {
|
|
149
|
+
parts.push(`agent=${overrides.agent}`);
|
|
150
|
+
}
|
|
151
|
+
if (overrides?.ide) {
|
|
152
|
+
parts.push('--ide connected');
|
|
153
|
+
}
|
|
154
|
+
const overrideClause =
|
|
155
|
+
parts.length > 0
|
|
156
|
+
? ` Restart-specific overrides applied: ${parts.join(', ')} — acknowledge briefly, then continue.`
|
|
157
|
+
: '';
|
|
158
|
+
return (
|
|
159
|
+
'<system-reminder>\n' +
|
|
160
|
+
'This session was restarted via fnc_restart (all prior context and the ' +
|
|
161
|
+
'session JSONL are preserved). Resume the work that was in flight ' +
|
|
162
|
+
'before the restart — finish the task, monitor what you were ' +
|
|
163
|
+
'monitoring, surface results — rather than treating this as a fresh ' +
|
|
164
|
+
'session.' +
|
|
165
|
+
overrideClause +
|
|
166
|
+
'\n</system-reminder>'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Append an `isMeta:true` user-message bearing a `<system-reminder>` block
|
|
172
|
+
* to the session JSONL at `launchCWD` / `sessionID`. Best-effort: missing
|
|
173
|
+
* or unreadable JSONL is silently tolerated (the restart should still
|
|
174
|
+
* proceed; the reminder is a UX nicety, not a hard requirement).
|
|
175
|
+
*
|
|
176
|
+
* Shape matches the entries Claude Code itself emits for inline reminders
|
|
177
|
+
* — `type:"user"`, `message:{role:"user",content:"<system-reminder>…</system-reminder>"}`,
|
|
178
|
+
* `isMeta:true`. The `parentUuid` is linked to the most recent entry's
|
|
179
|
+
* `uuid` so the resumed session reads it as a fresh terminal user turn.
|
|
180
|
+
*/
|
|
181
|
+
export function appendRestartReminder(
|
|
182
|
+
launchCWD: string,
|
|
183
|
+
sessionID: string,
|
|
184
|
+
overrides?: RestartReminderOverrides,
|
|
185
|
+
): void {
|
|
186
|
+
const path = sessionJSONLPath(launchCWD, sessionID);
|
|
187
|
+
let existing: string;
|
|
188
|
+
try {
|
|
189
|
+
existing = readFileSync(path, 'utf8');
|
|
190
|
+
} catch {
|
|
191
|
+
// No JSONL — nothing to append to. The relaunched claude will start
|
|
192
|
+
// fresh anyway, so the reminder would be off-target.
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const parentUuid = lastEntryUUID(existing);
|
|
196
|
+
const entry = {
|
|
197
|
+
parentUuid,
|
|
198
|
+
isSidechain: false,
|
|
199
|
+
type: 'user' as const,
|
|
200
|
+
message: {
|
|
201
|
+
role: 'user' as const,
|
|
202
|
+
content: renderRestartReminderContent(overrides),
|
|
203
|
+
},
|
|
204
|
+
isMeta: true,
|
|
205
|
+
uuid: randomUUID(),
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
userType: 'external' as const,
|
|
208
|
+
cwd: launchCWD,
|
|
209
|
+
sessionId: sessionID,
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`);
|
|
213
|
+
} catch {
|
|
214
|
+
// Best-effort — disk full, permission denied, raced unlink, etc. The
|
|
215
|
+
// restart proceeds; user gets the historical "Restarted." idle behavior
|
|
216
|
+
// rather than a hard failure.
|
|
217
|
+
}
|
|
218
|
+
}
|