@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnclaude/cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "fnclaude CLI implementation (TypeScript rewrite, in progress)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 { readLivePermissionMode } from '../sessionState.js';
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' };
@@ -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
+ }