@fnclaude/cli 0.7.8 → 1.0.0
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/config.ts +23 -20
- package/src/errors.ts +13 -0
- package/src/main.ts +21 -20
- package/src/mcp/client.ts +22 -22
- package/src/mcp/protocol.ts +12 -12
- package/src/mcp/socketListener.ts +20 -20
- package/src/pty/unix.ts +25 -22
- package/src/pty/windows.ts +18 -17
- package/src/pty.ts +18 -17
- package/src/repoRef.ts +17 -15
- package/src/repoSettings.ts +12 -11
- package/src/sanitize.ts +12 -12
- package/src/sessionState.ts +12 -10
- package/src/worktree.ts +6 -6
package/src/repoSettings.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { homedir, platform } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { errorMessage } from './errors.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* fnclaude's view of the shared `repoSettings` block. Only the keys
|
|
@@ -80,7 +81,7 @@ export function mergeRepoSettings(paths: string[]): LoadRepoSettingsResult {
|
|
|
80
81
|
const warnings: string[] = [];
|
|
81
82
|
for (const p of paths) {
|
|
82
83
|
const { settings: f, warning } = readRepoSettings(p);
|
|
83
|
-
if (warning !==
|
|
84
|
+
if (warning !== undefined) warnings.push(warning);
|
|
84
85
|
if (!f) continue;
|
|
85
86
|
// Shallow-merge per field: only overwrite when the higher tier sets
|
|
86
87
|
// a non-empty value.
|
|
@@ -93,8 +94,8 @@ export function mergeRepoSettings(paths: string[]): LoadRepoSettingsResult {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
interface ReadRepoSettingsResult {
|
|
96
|
-
settings: RepoSettings |
|
|
97
|
-
warning: string |
|
|
97
|
+
settings: RepoSettings | undefined;
|
|
98
|
+
warning: string | undefined;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function readRepoSettings(path: string): ReadRepoSettingsResult {
|
|
@@ -103,25 +104,25 @@ function readRepoSettings(path: string): ReadRepoSettingsResult {
|
|
|
103
104
|
data = readFileSync(path, 'utf8');
|
|
104
105
|
} catch {
|
|
105
106
|
// Missing file is the common path — stay silent.
|
|
106
|
-
return { settings:
|
|
107
|
+
return { settings: undefined, warning: undefined };
|
|
107
108
|
}
|
|
108
109
|
let f: SettingsFile;
|
|
109
110
|
try {
|
|
110
111
|
f = JSON.parse(data) as SettingsFile;
|
|
111
112
|
} catch (err) {
|
|
112
113
|
return {
|
|
113
|
-
settings:
|
|
114
|
-
warning: `fnclaude: repo-settings file ${path} is malformed, skipping: ${(err
|
|
114
|
+
settings: undefined,
|
|
115
|
+
warning: `fnclaude: repo-settings file ${path} is malformed, skipping: ${errorMessage(err)}`,
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
|
-
return { settings: f.repoSettings ??
|
|
118
|
+
return { settings: f.repoSettings ?? undefined, warning: undefined };
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/**
|
|
121
122
|
* Platform-specific path to Claude Code's managed-settings.json, or
|
|
122
|
-
* `
|
|
123
|
+
* `undefined` on platforms with no such convention.
|
|
123
124
|
*/
|
|
124
|
-
export function managedSettingsPath(): string |
|
|
125
|
+
export function managedSettingsPath(): string | undefined {
|
|
125
126
|
switch (platform()) {
|
|
126
127
|
case 'linux':
|
|
127
128
|
return '/etc/claude-code/managed-settings.json';
|
|
@@ -130,10 +131,10 @@ export function managedSettingsPath(): string | null {
|
|
|
130
131
|
case 'win32': {
|
|
131
132
|
const pd = process.env.ProgramData;
|
|
132
133
|
if (pd) return join(pd, 'ClaudeCode', 'managed-settings.json');
|
|
133
|
-
return
|
|
134
|
+
return undefined;
|
|
134
135
|
}
|
|
135
136
|
default:
|
|
136
|
-
return
|
|
137
|
+
return undefined;
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
package/src/sanitize.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// '/' is allowed so git-style nested refs (feat/foo, team/x/y/z) pass
|
|
8
8
|
// through and produce nested worktree paths.
|
|
9
9
|
//
|
|
10
|
-
// Returns
|
|
10
|
+
// Returns undefined when:
|
|
11
11
|
// - the input is empty
|
|
12
12
|
// - the input starts with '/' (would escape the configured path prefix)
|
|
13
13
|
// - the result reduces to empty after sanitization
|
|
@@ -21,9 +21,9 @@ const RE_PATH_SAFE_BAD = /[^A-Za-z0-9._/-]+/g;
|
|
|
21
21
|
const RE_DASH_RUN = /-{2,}/g;
|
|
22
22
|
const RE_SLASH_RUN = /\/{2,}/g;
|
|
23
23
|
|
|
24
|
-
export function sanitizeName(s: string): string |
|
|
25
|
-
if (s === '') return
|
|
26
|
-
if (s.startsWith('/')) return
|
|
24
|
+
export function sanitizeName(s: string): string | undefined {
|
|
25
|
+
if (s === '') return undefined;
|
|
26
|
+
if (s.startsWith('/')) return undefined;
|
|
27
27
|
|
|
28
28
|
let out = s.replace(RE_PATH_SAFE_BAD, '-');
|
|
29
29
|
out = out.replace(RE_DASH_RUN, '-');
|
|
@@ -31,8 +31,8 @@ export function sanitizeName(s: string): string | null {
|
|
|
31
31
|
out = trimLeftAny(out, '-.');
|
|
32
32
|
out = trimRightAny(out, '-/');
|
|
33
33
|
|
|
34
|
-
if (out === '') return
|
|
35
|
-
if (out.includes('..')) return
|
|
34
|
+
if (out === '') return undefined;
|
|
35
|
+
if (out.includes('..')) return undefined;
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -71,7 +71,7 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
|
|
|
71
71
|
if ((t === '--name' || t === '-n') && i + 1 < out.length) {
|
|
72
72
|
const val = out[i + 1]!;
|
|
73
73
|
const decision = decideSanitize(val);
|
|
74
|
-
if (decision.warning !==
|
|
74
|
+
if (decision.warning !== undefined) warnings.push(decision.warning);
|
|
75
75
|
if (decision.replace) out[i + 1] = decision.cleaned;
|
|
76
76
|
i++; // skip the value slot
|
|
77
77
|
continue;
|
|
@@ -79,14 +79,14 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
|
|
|
79
79
|
if (t.startsWith('--name=')) {
|
|
80
80
|
const val = t.slice('--name='.length);
|
|
81
81
|
const decision = decideSanitize(val);
|
|
82
|
-
if (decision.warning !==
|
|
82
|
+
if (decision.warning !== undefined) warnings.push(decision.warning);
|
|
83
83
|
if (decision.replace) out[i] = `--name=${decision.cleaned}`;
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
if (t.startsWith('-n=')) {
|
|
87
87
|
const val = t.slice('-n='.length);
|
|
88
88
|
const decision = decideSanitize(val);
|
|
89
|
-
if (decision.warning !==
|
|
89
|
+
if (decision.warning !== undefined) warnings.push(decision.warning);
|
|
90
90
|
if (decision.replace) out[i] = `-n=${decision.cleaned}`;
|
|
91
91
|
continue;
|
|
92
92
|
}
|
|
@@ -96,13 +96,13 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
|
|
|
96
96
|
|
|
97
97
|
interface SanitizeDecision {
|
|
98
98
|
cleaned: string;
|
|
99
|
-
warning: string |
|
|
99
|
+
warning: string | undefined;
|
|
100
100
|
replace: boolean;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function decideSanitize(val: string): SanitizeDecision {
|
|
104
104
|
const cleaned = sanitizeName(val);
|
|
105
|
-
if (cleaned ===
|
|
105
|
+
if (cleaned === undefined) {
|
|
106
106
|
return {
|
|
107
107
|
cleaned: val,
|
|
108
108
|
warning: `fnclaude: --name ${JSON.stringify(val)} has no path-safe characters; passing through unchanged`,
|
|
@@ -110,7 +110,7 @@ function decideSanitize(val: string): SanitizeDecision {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
if (cleaned === val) {
|
|
113
|
-
return { cleaned: '', warning:
|
|
113
|
+
return { cleaned: '', warning: undefined, replace: false };
|
|
114
114
|
}
|
|
115
115
|
return {
|
|
116
116
|
cleaned,
|
package/src/sessionState.ts
CHANGED
|
@@ -87,7 +87,7 @@ export function readLivePermissionMode(
|
|
|
87
87
|
} catch {
|
|
88
88
|
continue; // malformed line — ignore
|
|
89
89
|
}
|
|
90
|
-
if (r.type === 'permission-mode' && typeof r.permissionMode === 'string' && r.permissionMode
|
|
90
|
+
if (r.type === 'permission-mode' && typeof r.permissionMode === 'string' && r.permissionMode) {
|
|
91
91
|
latest = r.permissionMode;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -110,10 +110,10 @@ export interface RestartReminderOverrides {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* Read the trailing entries of `data` and return the most recent `uuid`
|
|
113
|
-
* field, or `
|
|
114
|
-
* the JSONL parent-chain.
|
|
113
|
+
* field, or `undefined` if none found. Used to link the appended reminder
|
|
114
|
+
* into the JSONL parent-chain.
|
|
115
115
|
*/
|
|
116
|
-
function lastEntryUUID(data: string): string |
|
|
116
|
+
function lastEntryUUID(data: string): string | undefined {
|
|
117
117
|
const lines = data.split('\n');
|
|
118
118
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
119
119
|
const line = lines[i];
|
|
@@ -128,7 +128,7 @@ function lastEntryUUID(data: string): string | null {
|
|
|
128
128
|
return parsed.uuid;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
return
|
|
131
|
+
return undefined;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/** Render the system-reminder body text, optionally naming overrides. */
|
|
@@ -136,16 +136,16 @@ export function renderRestartReminderContent(
|
|
|
136
136
|
overrides?: RestartReminderOverrides,
|
|
137
137
|
): string {
|
|
138
138
|
const parts: string[] = [];
|
|
139
|
-
if (overrides?.model
|
|
139
|
+
if (overrides?.model) {
|
|
140
140
|
parts.push(`model swap to ${overrides.model}`);
|
|
141
141
|
}
|
|
142
|
-
if (overrides?.effort
|
|
142
|
+
if (overrides?.effort) {
|
|
143
143
|
parts.push(`effort=${overrides.effort}`);
|
|
144
144
|
}
|
|
145
|
-
if (overrides?.permissionMode
|
|
145
|
+
if (overrides?.permissionMode) {
|
|
146
146
|
parts.push(`permission-mode=${overrides.permissionMode}`);
|
|
147
147
|
}
|
|
148
|
-
if (overrides?.agent
|
|
148
|
+
if (overrides?.agent) {
|
|
149
149
|
parts.push(`agent=${overrides.agent}`);
|
|
150
150
|
}
|
|
151
151
|
if (overrides?.ide) {
|
|
@@ -192,7 +192,9 @@ export function appendRestartReminder(
|
|
|
192
192
|
// fresh anyway, so the reminder would be off-target.
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
// JSONL parentUuid is on the wire — keep null encoding for "no parent"
|
|
196
|
+
// entries to match Claude Code's own writer.
|
|
197
|
+
const parentUuid = lastEntryUUID(existing) ?? null;
|
|
196
198
|
const entry = {
|
|
197
199
|
parentUuid,
|
|
198
200
|
isSidechain: false,
|
package/src/worktree.ts
CHANGED
|
@@ -114,15 +114,15 @@ export function listWorktrees(dir: string, runner: GitRunner = defaultGitRunner)
|
|
|
114
114
|
* 3. Basename of the path == query (last-resort fallback for worktrees
|
|
115
115
|
* whose branch was renamed or whose creator skipped the convention)
|
|
116
116
|
*
|
|
117
|
-
* Returns
|
|
118
|
-
*
|
|
119
|
-
* accident.
|
|
117
|
+
* Returns undefined when no entry matches. Undefined `query` short-circuits
|
|
118
|
+
* to undefined so that detached worktrees (branch=undefined) can't be
|
|
119
|
+
* matched by accident.
|
|
120
120
|
*/
|
|
121
121
|
export function findWorktree(
|
|
122
122
|
worktrees: readonly WorktreeInfo[],
|
|
123
123
|
query: string | undefined,
|
|
124
|
-
): WorktreeInfo |
|
|
125
|
-
if (query === undefined) return
|
|
124
|
+
): WorktreeInfo | undefined {
|
|
125
|
+
if (query === undefined) return undefined;
|
|
126
126
|
|
|
127
127
|
for (const wt of worktrees) {
|
|
128
128
|
if (wt.branch === query) return wt;
|
|
@@ -133,7 +133,7 @@ export function findWorktree(
|
|
|
133
133
|
for (const wt of worktrees) {
|
|
134
134
|
if (basename(wt.path) === query) return wt;
|
|
135
135
|
}
|
|
136
|
-
return
|
|
136
|
+
return undefined;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function stripWorktreePrefix(branch: string): string {
|