claudeup 4.2.0 → 4.4.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/data/settings-catalog.js +18 -0
- package/src/data/settings-catalog.ts +24 -1
- package/src/services/claude-settings.js +31 -3
- package/src/services/claude-settings.ts +32 -3
- package/src/services/settings-manager.js +58 -2
- package/src/services/settings-manager.ts +52 -2
- package/src/ui/renderers/settingsRenderers.js +7 -3
- package/src/ui/renderers/settingsRenderers.tsx +7 -3
package/package.json
CHANGED
|
@@ -162,6 +162,24 @@ export const SETTINGS_CATALOG = [
|
|
|
162
162
|
storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
|
|
163
163
|
defaultValue: "false",
|
|
164
164
|
},
|
|
165
|
+
{
|
|
166
|
+
id: "attribution",
|
|
167
|
+
name: "AI Attribution in Commits & PRs",
|
|
168
|
+
description: "Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
|
|
169
|
+
category: "workflow",
|
|
170
|
+
type: "boolean",
|
|
171
|
+
storage: { type: "attribution" },
|
|
172
|
+
defaultValue: "true",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "attribution-text",
|
|
176
|
+
name: "Custom Attribution Text",
|
|
177
|
+
description: "Custom text for commit and PR attribution. Applied when AI Attribution is enabled. Leave empty to use Claude's default",
|
|
178
|
+
category: "workflow",
|
|
179
|
+
type: "string",
|
|
180
|
+
storage: { type: "attribution-text" },
|
|
181
|
+
defaultValue: "Crafted with agentic harness Magus (https://github.com/MadAppGang/magus)",
|
|
182
|
+
},
|
|
165
183
|
{
|
|
166
184
|
id: "output-style",
|
|
167
185
|
name: "Output Style",
|
|
@@ -9,7 +9,9 @@ export type SettingCategory =
|
|
|
9
9
|
export type SettingType = "boolean" | "string" | "select";
|
|
10
10
|
export type SettingStorage =
|
|
11
11
|
| { type: "env"; key: string }
|
|
12
|
-
| { type: "setting"; key: string }
|
|
12
|
+
| { type: "setting"; key: string }
|
|
13
|
+
| { type: "attribution" }
|
|
14
|
+
| { type: "attribution-text" };
|
|
13
15
|
|
|
14
16
|
export interface SettingDefinition {
|
|
15
17
|
id: string;
|
|
@@ -204,6 +206,27 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
204
206
|
storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
|
|
205
207
|
defaultValue: "false",
|
|
206
208
|
},
|
|
209
|
+
{
|
|
210
|
+
id: "attribution",
|
|
211
|
+
name: "AI Attribution in Commits & PRs",
|
|
212
|
+
description:
|
|
213
|
+
"Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
|
|
214
|
+
category: "workflow",
|
|
215
|
+
type: "boolean",
|
|
216
|
+
storage: { type: "attribution" },
|
|
217
|
+
defaultValue: "true",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "attribution-text",
|
|
221
|
+
name: "Custom Attribution Text",
|
|
222
|
+
description:
|
|
223
|
+
"Custom text for commit and PR attribution. Applied when AI Attribution is enabled. Leave empty to use Claude's default",
|
|
224
|
+
category: "workflow",
|
|
225
|
+
type: "string",
|
|
226
|
+
storage: { type: "attribution-text" },
|
|
227
|
+
defaultValue:
|
|
228
|
+
"Crafted with agentic harness Magus (https://github.com/MadAppGang/magus)",
|
|
229
|
+
},
|
|
207
230
|
{
|
|
208
231
|
id: "output-style",
|
|
209
232
|
name: "Output Style",
|
|
@@ -496,6 +496,35 @@ function migrateSettingsObject(settings) {
|
|
|
496
496
|
* Runs across project settings, global settings, local settings,
|
|
497
497
|
* known_marketplaces.json, and installed_plugins.json.
|
|
498
498
|
*/
|
|
499
|
+
/**
|
|
500
|
+
* Decode a ~/.claude/projects/ directory name back to a filesystem path.
|
|
501
|
+
* Claude Code encodes paths by replacing "/" with "-", which is lossy when
|
|
502
|
+
* directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
|
|
503
|
+
* Strategy: split on "-", then greedily recombine segments by checking which
|
|
504
|
+
* combinations actually exist on disk.
|
|
505
|
+
* Returns null if no valid path can be resolved.
|
|
506
|
+
*/
|
|
507
|
+
async function decodeProjectDirName(encoded) {
|
|
508
|
+
// Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
|
|
509
|
+
const segments = encoded.replace(/^-/, "").split("-");
|
|
510
|
+
if (segments.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
// Build path greedily: at each step, try joining the next segment with a dash first
|
|
513
|
+
// (preserving directory names like "circl-infra"), fall back to slash (new path segment)
|
|
514
|
+
let current = "/" + segments[0];
|
|
515
|
+
for (let i = 1; i < segments.length; i++) {
|
|
516
|
+
const withDash = current + "-" + segments[i];
|
|
517
|
+
const withSlash = current + "/" + segments[i];
|
|
518
|
+
// Prefer dash (keeps compound names intact) if that path prefix exists
|
|
519
|
+
if (await fs.pathExists(withDash)) {
|
|
520
|
+
current = withDash;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
current = withSlash;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return (await fs.pathExists(current)) ? current : null;
|
|
527
|
+
}
|
|
499
528
|
export async function migrateMarketplaceRename(projectPath) {
|
|
500
529
|
const result = {
|
|
501
530
|
projectMigrated: 0,
|
|
@@ -664,9 +693,8 @@ export async function migrateMarketplaceRename(projectPath) {
|
|
|
664
693
|
const currentProject = projectPath || process.cwd();
|
|
665
694
|
seenPaths.add(currentProject);
|
|
666
695
|
for (const entry of entries) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (seenPaths.has(decoded))
|
|
696
|
+
const decoded = await decodeProjectDirName(entry);
|
|
697
|
+
if (!decoded || seenPaths.has(decoded))
|
|
670
698
|
continue;
|
|
671
699
|
seenPaths.add(decoded);
|
|
672
700
|
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
@@ -731,6 +731,36 @@ export interface MigrationResult {
|
|
|
731
731
|
* Runs across project settings, global settings, local settings,
|
|
732
732
|
* known_marketplaces.json, and installed_plugins.json.
|
|
733
733
|
*/
|
|
734
|
+
/**
|
|
735
|
+
* Decode a ~/.claude/projects/ directory name back to a filesystem path.
|
|
736
|
+
* Claude Code encodes paths by replacing "/" with "-", which is lossy when
|
|
737
|
+
* directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
|
|
738
|
+
* Strategy: split on "-", then greedily recombine segments by checking which
|
|
739
|
+
* combinations actually exist on disk.
|
|
740
|
+
* Returns null if no valid path can be resolved.
|
|
741
|
+
*/
|
|
742
|
+
async function decodeProjectDirName(encoded: string): Promise<string | null> {
|
|
743
|
+
// Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
|
|
744
|
+
const segments = encoded.replace(/^-/, "").split("-");
|
|
745
|
+
if (segments.length === 0) return null;
|
|
746
|
+
|
|
747
|
+
// Build path greedily: at each step, try joining the next segment with a dash first
|
|
748
|
+
// (preserving directory names like "circl-infra"), fall back to slash (new path segment)
|
|
749
|
+
let current = "/" + segments[0];
|
|
750
|
+
for (let i = 1; i < segments.length; i++) {
|
|
751
|
+
const withDash = current + "-" + segments[i];
|
|
752
|
+
const withSlash = current + "/" + segments[i];
|
|
753
|
+
// Prefer dash (keeps compound names intact) if that path prefix exists
|
|
754
|
+
if (await fs.pathExists(withDash)) {
|
|
755
|
+
current = withDash;
|
|
756
|
+
} else {
|
|
757
|
+
current = withSlash;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return (await fs.pathExists(current)) ? current : null;
|
|
762
|
+
}
|
|
763
|
+
|
|
734
764
|
export async function migrateMarketplaceRename(
|
|
735
765
|
projectPath?: string,
|
|
736
766
|
): Promise<MigrationResult> {
|
|
@@ -914,9 +944,8 @@ export async function migrateMarketplaceRename(
|
|
|
914
944
|
seenPaths.add(currentProject);
|
|
915
945
|
|
|
916
946
|
for (const entry of entries) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (seenPaths.has(decoded)) continue;
|
|
947
|
+
const decoded = await decodeProjectDirName(entry);
|
|
948
|
+
if (!decoded || seenPaths.has(decoded)) continue;
|
|
920
949
|
seenPaths.add(decoded);
|
|
921
950
|
|
|
922
951
|
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
@@ -4,7 +4,36 @@ export async function readSettingValue(setting, scope, projectPath) {
|
|
|
4
4
|
const settings = scope === "user"
|
|
5
5
|
? await readGlobalSettings()
|
|
6
6
|
: await readSettings(projectPath);
|
|
7
|
-
if (setting.storage.type === "
|
|
7
|
+
if (setting.storage.type === "attribution") {
|
|
8
|
+
// Attribution is an object: { commit: "", pr: "" } means disabled
|
|
9
|
+
const attr = settings.attribution;
|
|
10
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
11
|
+
return "false";
|
|
12
|
+
}
|
|
13
|
+
return undefined; // default (enabled)
|
|
14
|
+
}
|
|
15
|
+
else if (setting.storage.type === "attribution-text") {
|
|
16
|
+
// Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
|
|
17
|
+
const attr = settings.attribution;
|
|
18
|
+
if (!attr || (attr.commit === "" && attr.pr === "")) {
|
|
19
|
+
// Attribution is disabled or not set — no custom text stored
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const commit = attr.commit;
|
|
23
|
+
if (!commit) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
|
|
27
|
+
// Strip the trailer prefix if present
|
|
28
|
+
const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
|
|
29
|
+
if (commit.startsWith(trailerPrefix)) {
|
|
30
|
+
const text = commit.slice(trailerPrefix.length);
|
|
31
|
+
return text.length > 0 ? text : undefined;
|
|
32
|
+
}
|
|
33
|
+
// If no trailer prefix, the value is the raw text (or Claude default — return undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
else if (setting.storage.type === "env") {
|
|
8
37
|
const env = settings.env;
|
|
9
38
|
return env?.[setting.storage.key];
|
|
10
39
|
}
|
|
@@ -23,7 +52,34 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
|
|
|
23
52
|
const settings = scope === "user"
|
|
24
53
|
? await readGlobalSettings()
|
|
25
54
|
: await readSettings(projectPath);
|
|
26
|
-
if (setting.storage.type === "
|
|
55
|
+
if (setting.storage.type === "attribution") {
|
|
56
|
+
// Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
|
|
57
|
+
if (value === "false") {
|
|
58
|
+
settings.attribution = { commit: "", pr: "" };
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
delete settings.attribution;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (setting.storage.type === "attribution-text") {
|
|
65
|
+
const attr = settings.attribution;
|
|
66
|
+
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
67
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (value && value.trim().length > 0) {
|
|
71
|
+
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
72
|
+
settings.attribution = {
|
|
73
|
+
commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
|
|
74
|
+
pr: value,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Empty value: remove attribution key entirely (revert to Claude defaults)
|
|
79
|
+
delete settings.attribution;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (setting.storage.type === "env") {
|
|
27
83
|
// Write to the "env" block in settings.json
|
|
28
84
|
settings.env = settings.env || {};
|
|
29
85
|
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
@@ -19,7 +19,34 @@ export async function readSettingValue(
|
|
|
19
19
|
? await readGlobalSettings()
|
|
20
20
|
: await readSettings(projectPath);
|
|
21
21
|
|
|
22
|
-
if (setting.storage.type === "
|
|
22
|
+
if (setting.storage.type === "attribution") {
|
|
23
|
+
// Attribution is an object: { commit: "", pr: "" } means disabled
|
|
24
|
+
const attr = (settings as any).attribution;
|
|
25
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
26
|
+
return "false";
|
|
27
|
+
}
|
|
28
|
+
return undefined; // default (enabled)
|
|
29
|
+
} else if (setting.storage.type === "attribution-text") {
|
|
30
|
+
// Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
|
|
31
|
+
const attr = (settings as any).attribution;
|
|
32
|
+
if (!attr || (attr.commit === "" && attr.pr === "")) {
|
|
33
|
+
// Attribution is disabled or not set — no custom text stored
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const commit: string | undefined = attr.commit;
|
|
37
|
+
if (!commit) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
|
|
41
|
+
// Strip the trailer prefix if present
|
|
42
|
+
const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
|
|
43
|
+
if (commit.startsWith(trailerPrefix)) {
|
|
44
|
+
const text = commit.slice(trailerPrefix.length);
|
|
45
|
+
return text.length > 0 ? text : undefined;
|
|
46
|
+
}
|
|
47
|
+
// If no trailer prefix, the value is the raw text (or Claude default — return undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
} else if (setting.storage.type === "env") {
|
|
23
50
|
const env = (settings as any).env as Record<string, string> | undefined;
|
|
24
51
|
return env?.[setting.storage.key];
|
|
25
52
|
} else {
|
|
@@ -45,7 +72,30 @@ export async function writeSettingValue(
|
|
|
45
72
|
? await readGlobalSettings()
|
|
46
73
|
: await readSettings(projectPath);
|
|
47
74
|
|
|
48
|
-
if (setting.storage.type === "
|
|
75
|
+
if (setting.storage.type === "attribution") {
|
|
76
|
+
// Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
|
|
77
|
+
if (value === "false") {
|
|
78
|
+
(settings as any).attribution = { commit: "", pr: "" };
|
|
79
|
+
} else {
|
|
80
|
+
delete (settings as any).attribution;
|
|
81
|
+
}
|
|
82
|
+
} else if (setting.storage.type === "attribution-text") {
|
|
83
|
+
const attr = (settings as any).attribution;
|
|
84
|
+
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
85
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value && value.trim().length > 0) {
|
|
89
|
+
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
90
|
+
(settings as any).attribution = {
|
|
91
|
+
commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
|
|
92
|
+
pr: value,
|
|
93
|
+
};
|
|
94
|
+
} else {
|
|
95
|
+
// Empty value: remove attribution key entirely (revert to Claude defaults)
|
|
96
|
+
delete (settings as any).attribution;
|
|
97
|
+
}
|
|
98
|
+
} else if (setting.storage.type === "env") {
|
|
49
99
|
// Write to the "env" block in settings.json
|
|
50
100
|
(settings as any).env = (settings as any).env || {};
|
|
51
101
|
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
@@ -31,9 +31,13 @@ const settingRenderer = {
|
|
|
31
31
|
renderDetail: ({ item }) => {
|
|
32
32
|
const { setting } = item;
|
|
33
33
|
const scoped = item.scopedValues;
|
|
34
|
-
const storageDesc = setting.storage.type === "
|
|
35
|
-
?
|
|
36
|
-
:
|
|
34
|
+
const storageDesc = setting.storage.type === "attribution"
|
|
35
|
+
? "settings.json: attribution"
|
|
36
|
+
: setting.storage.type === "attribution-text"
|
|
37
|
+
? "settings.json: attribution"
|
|
38
|
+
: setting.storage.type === "env"
|
|
39
|
+
? `env: ${setting.storage.key}`
|
|
40
|
+
: `settings.json: ${setting.storage.key}`;
|
|
37
41
|
const userValue = formatValue(setting, scoped.user);
|
|
38
42
|
const projectValue = formatValue(setting, scoped.project);
|
|
39
43
|
const userIsSet = scoped.user !== undefined && scoped.user !== "";
|
|
@@ -79,9 +79,13 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
|
|
|
79
79
|
const { setting } = item;
|
|
80
80
|
const scoped = item.scopedValues;
|
|
81
81
|
const storageDesc =
|
|
82
|
-
setting.storage.type === "
|
|
83
|
-
?
|
|
84
|
-
:
|
|
82
|
+
setting.storage.type === "attribution"
|
|
83
|
+
? "settings.json: attribution"
|
|
84
|
+
: setting.storage.type === "attribution-text"
|
|
85
|
+
? "settings.json: attribution"
|
|
86
|
+
: setting.storage.type === "env"
|
|
87
|
+
? `env: ${setting.storage.key}`
|
|
88
|
+
: `settings.json: ${setting.storage.key}`;
|
|
85
89
|
|
|
86
90
|
const userValue = formatValue(setting, scoped.user);
|
|
87
91
|
const projectValue = formatValue(setting, scoped.project);
|