claudeup 6.6.0 → 6.7.1
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 +4 -4
- package/src/__tests__/conventions-manager.test.ts +45 -45
- package/src/__tests__/enabled-not-installed.test.ts +1 -1
- package/src/__tests__/models-presets-marketplace.test.ts +141 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
- package/src/__tests__/resolver.test.ts +3 -3
- package/src/data/models-presets.ts +10 -11
- package/src/data/predefined-profiles.ts +7 -7
- package/src/services/claude-settings.ts +1 -1
- package/src/services/community-styles.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeup",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.1",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^5.6.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"claudeup-darwin-arm64": "6.
|
|
68
|
-
"claudeup-darwin-x64": "6.
|
|
69
|
-
"claudeup-linux-x64": "6.
|
|
67
|
+
"claudeup-darwin-arm64": "6.7.1",
|
|
68
|
+
"claudeup-darwin-x64": "6.7.1",
|
|
69
|
+
"claudeup-linux-x64": "6.7.1"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -240,11 +240,11 @@ describe("conventions-manager", () => {
|
|
|
240
240
|
const content = [
|
|
241
241
|
"# Project CLAUDE.md",
|
|
242
242
|
"",
|
|
243
|
-
"<!-- BEGIN magus:code-
|
|
243
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
244
244
|
"## Code Analysis Conventions",
|
|
245
245
|
"",
|
|
246
246
|
"> Managed by plugin",
|
|
247
|
-
"<!-- END magus:code-
|
|
247
|
+
"<!-- END magus:code-search -->",
|
|
248
248
|
"",
|
|
249
249
|
"Other content",
|
|
250
250
|
].join("\n");
|
|
@@ -253,7 +253,7 @@ describe("conventions-manager", () => {
|
|
|
253
253
|
|
|
254
254
|
expect(result.errors).toEqual([]);
|
|
255
255
|
expect(result.sections).toHaveLength(1);
|
|
256
|
-
expect(result.sections[0].section).toBe("code-
|
|
256
|
+
expect(result.sections[0].section).toBe("code-search");
|
|
257
257
|
expect(result.sections[0].version).toBe("1.0.0");
|
|
258
258
|
expect(result.sections[0].content).toContain("Code Analysis Conventions");
|
|
259
259
|
expect(result.sections[0].startLine).toBe(3);
|
|
@@ -262,9 +262,9 @@ describe("conventions-manager", () => {
|
|
|
262
262
|
|
|
263
263
|
it("should parse multiple sections", () => {
|
|
264
264
|
const content = [
|
|
265
|
-
"<!-- BEGIN magus:code-
|
|
265
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
266
266
|
"Section 1 content",
|
|
267
|
-
"<!-- END magus:code-
|
|
267
|
+
"<!-- END magus:code-search -->",
|
|
268
268
|
"",
|
|
269
269
|
"<!-- BEGIN magus:terminal v2.0.0 -->",
|
|
270
270
|
"Section 2 content",
|
|
@@ -275,13 +275,13 @@ describe("conventions-manager", () => {
|
|
|
275
275
|
|
|
276
276
|
expect(result.errors).toEqual([]);
|
|
277
277
|
expect(result.sections).toHaveLength(2);
|
|
278
|
-
expect(result.sections[0].section).toBe("code-
|
|
278
|
+
expect(result.sections[0].section).toBe("code-search");
|
|
279
279
|
expect(result.sections[1].section).toBe("terminal");
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
it("should detect missing END marker", () => {
|
|
283
283
|
const content = [
|
|
284
|
-
"<!-- BEGIN magus:code-
|
|
284
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
285
285
|
"Some content without an end marker",
|
|
286
286
|
].join("\n");
|
|
287
287
|
|
|
@@ -289,18 +289,18 @@ describe("conventions-manager", () => {
|
|
|
289
289
|
|
|
290
290
|
expect(result.errors).toHaveLength(1);
|
|
291
291
|
expect(result.errors[0].type).toBe("missing_end");
|
|
292
|
-
expect(result.errors[0]).toHaveProperty("section", "code-
|
|
292
|
+
expect(result.errors[0]).toHaveProperty("section", "code-search");
|
|
293
293
|
});
|
|
294
294
|
|
|
295
295
|
it("should detect duplicate sections", () => {
|
|
296
296
|
const content = [
|
|
297
|
-
"<!-- BEGIN magus:code-
|
|
297
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
298
298
|
"First",
|
|
299
|
-
"<!-- END magus:code-
|
|
299
|
+
"<!-- END magus:code-search -->",
|
|
300
300
|
"",
|
|
301
|
-
"<!-- BEGIN magus:code-
|
|
301
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
302
302
|
"Second",
|
|
303
|
-
"<!-- END magus:code-
|
|
303
|
+
"<!-- END magus:code-search -->",
|
|
304
304
|
].join("\n");
|
|
305
305
|
|
|
306
306
|
const result = parseClaudeMdContent(content);
|
|
@@ -309,7 +309,7 @@ describe("conventions-manager", () => {
|
|
|
309
309
|
(e) => e.type === "duplicate_section",
|
|
310
310
|
);
|
|
311
311
|
expect(dupError).toBeDefined();
|
|
312
|
-
expect(dupError).toHaveProperty("section", "code-
|
|
312
|
+
expect(dupError).toHaveProperty("section", "code-search");
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
it("should ignore markers inside code blocks", () => {
|
|
@@ -473,9 +473,9 @@ describe("conventions-manager", () => {
|
|
|
473
473
|
const content = [
|
|
474
474
|
"# Project",
|
|
475
475
|
"",
|
|
476
|
-
"<!-- BEGIN magus:code-
|
|
476
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
477
477
|
"Conventions here",
|
|
478
|
-
"<!-- END magus:code-
|
|
478
|
+
"<!-- END magus:code-search -->",
|
|
479
479
|
].join("\n");
|
|
480
480
|
await writeFile(claudeMdPath, content);
|
|
481
481
|
|
|
@@ -505,7 +505,7 @@ describe("conventions-manager", () => {
|
|
|
505
505
|
|
|
506
506
|
const result = await injectClaudeMdSection(
|
|
507
507
|
claudeMdPath,
|
|
508
|
-
"code-
|
|
508
|
+
"code-search",
|
|
509
509
|
"1.0.0",
|
|
510
510
|
"## Code Analysis\n\nPrivate paths here",
|
|
511
511
|
);
|
|
@@ -513,9 +513,9 @@ describe("conventions-manager", () => {
|
|
|
513
513
|
expect(result.action).toBe("inserted");
|
|
514
514
|
|
|
515
515
|
const content = await readFile(claudeMdPath, "utf-8");
|
|
516
|
-
expect(content).toContain("<!-- BEGIN magus:code-
|
|
516
|
+
expect(content).toContain("<!-- BEGIN magus:code-search v1.0.0 -->");
|
|
517
517
|
expect(content).toContain("## Code Analysis");
|
|
518
|
-
expect(content).toContain("<!-- END magus:code-
|
|
518
|
+
expect(content).toContain("<!-- END magus:code-search -->");
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
it("should insert new section into existing file", async () => {
|
|
@@ -524,7 +524,7 @@ describe("conventions-manager", () => {
|
|
|
524
524
|
|
|
525
525
|
const result = await injectClaudeMdSection(
|
|
526
526
|
claudeMdPath,
|
|
527
|
-
"code-
|
|
527
|
+
"code-search",
|
|
528
528
|
"1.0.0",
|
|
529
529
|
"New content",
|
|
530
530
|
);
|
|
@@ -534,7 +534,7 @@ describe("conventions-manager", () => {
|
|
|
534
534
|
const content = await readFile(claudeMdPath, "utf-8");
|
|
535
535
|
expect(content).toContain("# Project");
|
|
536
536
|
expect(content).toContain("Existing content");
|
|
537
|
-
expect(content).toContain("<!-- BEGIN magus:code-
|
|
537
|
+
expect(content).toContain("<!-- BEGIN magus:code-search v1.0.0 -->");
|
|
538
538
|
});
|
|
539
539
|
|
|
540
540
|
it("should update section when version changes", async () => {
|
|
@@ -542,15 +542,15 @@ describe("conventions-manager", () => {
|
|
|
542
542
|
const initial = [
|
|
543
543
|
"# Project",
|
|
544
544
|
"",
|
|
545
|
-
"<!-- BEGIN magus:code-
|
|
545
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
546
546
|
"Old content",
|
|
547
|
-
"<!-- END magus:code-
|
|
547
|
+
"<!-- END magus:code-search -->",
|
|
548
548
|
].join("\n");
|
|
549
549
|
await writeFile(claudeMdPath, initial);
|
|
550
550
|
|
|
551
551
|
const result = await injectClaudeMdSection(
|
|
552
552
|
claudeMdPath,
|
|
553
|
-
"code-
|
|
553
|
+
"code-search",
|
|
554
554
|
"2.0.0",
|
|
555
555
|
"New content",
|
|
556
556
|
);
|
|
@@ -568,15 +568,15 @@ describe("conventions-manager", () => {
|
|
|
568
568
|
it("should skip when version is unchanged", async () => {
|
|
569
569
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
570
570
|
const initial = [
|
|
571
|
-
"<!-- BEGIN magus:code-
|
|
571
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
572
572
|
"Current content",
|
|
573
|
-
"<!-- END magus:code-
|
|
573
|
+
"<!-- END magus:code-search -->",
|
|
574
574
|
].join("\n");
|
|
575
575
|
await writeFile(claudeMdPath, initial);
|
|
576
576
|
|
|
577
577
|
const result = await injectClaudeMdSection(
|
|
578
578
|
claudeMdPath,
|
|
579
|
-
"code-
|
|
579
|
+
"code-search",
|
|
580
580
|
"1.0.0",
|
|
581
581
|
"Different content but same version",
|
|
582
582
|
);
|
|
@@ -598,7 +598,7 @@ describe("conventions-manager", () => {
|
|
|
598
598
|
|
|
599
599
|
const result = await injectClaudeMdSection(
|
|
600
600
|
claudeMdPath,
|
|
601
|
-
"code-
|
|
601
|
+
"code-search",
|
|
602
602
|
"1.0.0",
|
|
603
603
|
"New content",
|
|
604
604
|
);
|
|
@@ -610,13 +610,13 @@ describe("conventions-manager", () => {
|
|
|
610
610
|
it("should refuse when file has duplicate sections", async () => {
|
|
611
611
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
612
612
|
const duplicate = [
|
|
613
|
-
"<!-- BEGIN magus:code-
|
|
613
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
614
614
|
"First",
|
|
615
|
-
"<!-- END magus:code-
|
|
615
|
+
"<!-- END magus:code-search -->",
|
|
616
616
|
"",
|
|
617
|
-
"<!-- BEGIN magus:code-
|
|
617
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
618
618
|
"Second",
|
|
619
|
-
"<!-- END magus:code-
|
|
619
|
+
"<!-- END magus:code-search -->",
|
|
620
620
|
].join("\n");
|
|
621
621
|
await writeFile(claudeMdPath, duplicate);
|
|
622
622
|
|
|
@@ -642,21 +642,21 @@ describe("conventions-manager", () => {
|
|
|
642
642
|
const content = [
|
|
643
643
|
"# Project",
|
|
644
644
|
"",
|
|
645
|
-
"<!-- BEGIN magus:code-
|
|
645
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
646
646
|
"Conventions here",
|
|
647
|
-
"<!-- END magus:code-
|
|
647
|
+
"<!-- END magus:code-search -->",
|
|
648
648
|
"",
|
|
649
649
|
"Other content",
|
|
650
650
|
].join("\n");
|
|
651
651
|
await writeFile(claudeMdPath, content);
|
|
652
652
|
|
|
653
|
-
const result = await removeClaudeMdSection(claudeMdPath, "code-
|
|
653
|
+
const result = await removeClaudeMdSection(claudeMdPath, "code-search");
|
|
654
654
|
|
|
655
655
|
expect(result.removed).toBe(true);
|
|
656
656
|
|
|
657
657
|
const newContent = await readFile(claudeMdPath, "utf-8");
|
|
658
|
-
expect(newContent).not.toContain("BEGIN magus:code-
|
|
659
|
-
expect(newContent).not.toContain("END magus:code-
|
|
658
|
+
expect(newContent).not.toContain("BEGIN magus:code-search");
|
|
659
|
+
expect(newContent).not.toContain("END magus:code-search");
|
|
660
660
|
expect(newContent).toContain("# Project");
|
|
661
661
|
expect(newContent).toContain("Other content");
|
|
662
662
|
});
|
|
@@ -665,7 +665,7 @@ describe("conventions-manager", () => {
|
|
|
665
665
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
666
666
|
await writeFile(claudeMdPath, "# Project\n\nSome content\n");
|
|
667
667
|
|
|
668
|
-
const result = await removeClaudeMdSection(claudeMdPath, "code-
|
|
668
|
+
const result = await removeClaudeMdSection(claudeMdPath, "code-search");
|
|
669
669
|
|
|
670
670
|
expect(result.removed).toBe(false);
|
|
671
671
|
});
|
|
@@ -673,7 +673,7 @@ describe("conventions-manager", () => {
|
|
|
673
673
|
it("should no-op for non-existent file", async () => {
|
|
674
674
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
675
675
|
|
|
676
|
-
const result = await removeClaudeMdSection(claudeMdPath, "code-
|
|
676
|
+
const result = await removeClaudeMdSection(claudeMdPath, "code-search");
|
|
677
677
|
|
|
678
678
|
expect(result.removed).toBe(false);
|
|
679
679
|
});
|
|
@@ -724,9 +724,9 @@ describe("conventions-manager", () => {
|
|
|
724
724
|
it("should preserve other sections when removing one", async () => {
|
|
725
725
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
726
726
|
const content = [
|
|
727
|
-
"<!-- BEGIN magus:code-
|
|
727
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
728
728
|
"First section",
|
|
729
|
-
"<!-- END magus:code-
|
|
729
|
+
"<!-- END magus:code-search -->",
|
|
730
730
|
"",
|
|
731
731
|
"<!-- BEGIN magus:terminal v2.0.0 -->",
|
|
732
732
|
"Second section",
|
|
@@ -734,10 +734,10 @@ describe("conventions-manager", () => {
|
|
|
734
734
|
].join("\n");
|
|
735
735
|
await writeFile(claudeMdPath, content);
|
|
736
736
|
|
|
737
|
-
await removeClaudeMdSection(claudeMdPath, "code-
|
|
737
|
+
await removeClaudeMdSection(claudeMdPath, "code-search");
|
|
738
738
|
|
|
739
739
|
const newContent = await readFile(claudeMdPath, "utf-8");
|
|
740
|
-
expect(newContent).not.toContain("code-
|
|
740
|
+
expect(newContent).not.toContain("code-search");
|
|
741
741
|
expect(newContent).toContain("<!-- BEGIN magus:terminal v2.0.0 -->");
|
|
742
742
|
expect(newContent).toContain("Second section");
|
|
743
743
|
});
|
|
@@ -751,9 +751,9 @@ describe("conventions-manager", () => {
|
|
|
751
751
|
it("should list all managed sections", async () => {
|
|
752
752
|
const claudeMdPath = join(testDir, "CLAUDE.md");
|
|
753
753
|
const content = [
|
|
754
|
-
"<!-- BEGIN magus:code-
|
|
754
|
+
"<!-- BEGIN magus:code-search v1.0.0 -->",
|
|
755
755
|
"First",
|
|
756
|
-
"<!-- END magus:code-
|
|
756
|
+
"<!-- END magus:code-search -->",
|
|
757
757
|
"",
|
|
758
758
|
"<!-- BEGIN magus:terminal v2.1.0 -->",
|
|
759
759
|
"Second",
|
|
@@ -764,7 +764,7 @@ describe("conventions-manager", () => {
|
|
|
764
764
|
const sections = await listClaudeMdSections(claudeMdPath);
|
|
765
765
|
|
|
766
766
|
expect(sections).toHaveLength(2);
|
|
767
|
-
expect(sections[0].section).toBe("code-
|
|
767
|
+
expect(sections[0].section).toBe("code-search");
|
|
768
768
|
expect(sections[0].version).toBe("1.0.0");
|
|
769
769
|
expect(sections[1].section).toBe("terminal");
|
|
770
770
|
expect(sections[1].version).toBe("2.1.0");
|
|
@@ -25,7 +25,7 @@ const plugin = (over: Partial<PluginInfo>): PluginInfo =>
|
|
|
25
25
|
|
|
26
26
|
describe("isEnabledButNotInstalled", () => {
|
|
27
27
|
test("enabled in project scope with no installed version is flagged", () => {
|
|
28
|
-
// The real case: terminal/code-
|
|
28
|
+
// The real case: terminal/code-search are in this project's enabledPlugins
|
|
29
29
|
// but have no registry entry for it and none at user scope.
|
|
30
30
|
expect(
|
|
31
31
|
isEnabledButNotInstalled(
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent tier table is keyed by agent id, and nothing else checks those ids.
|
|
3
|
+
*
|
|
4
|
+
* `DEFAULT_AGENT_GRADES` in `data/models-presets.ts` names
|
|
5
|
+
* agents as `plugin:name`. A rename in the marketplace (`code-search:analyze` →
|
|
6
|
+
* something else, `dev:synthesizer` → `dev:aggregator`) breaks the key with no signal: the
|
|
7
|
+
* renamed agent silently falls to the preset's fallback, and the stale key routes nothing.
|
|
8
|
+
* This test reads the marketplace source tree beside this repo and fails on either drift.
|
|
9
|
+
*
|
|
10
|
+
* Two directions:
|
|
11
|
+
* (a) every preset key names an agent that exists (or is a Claude Code built-in);
|
|
12
|
+
* (b) every marketplace agent is classified — in the tier table, in the frontmatter map,
|
|
13
|
+
* a vision agent, or explicitly left to the fallback in FALLBACK_BY_DESIGN below.
|
|
14
|
+
* Absence is a legitimate routing choice (fallback = `normal`), but it must be a
|
|
15
|
+
* choice: a new agent fails here until someone decides.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, test } from "bun:test";
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
19
|
+
import { join, resolve } from "node:path";
|
|
20
|
+
import { DEFAULT_AGENT_GRADES, VISION_AGENTS } from "../data/models-presets.js";
|
|
21
|
+
import { PREDEFINED_PROFILES } from "../data/predefined-profiles.js";
|
|
22
|
+
|
|
23
|
+
const REPO = resolve(import.meta.dir, "..", "..", "..", "..");
|
|
24
|
+
const MARKETPLACE = join(REPO, ".claude-plugin", "marketplace.json");
|
|
25
|
+
const inMagusSrc = existsSync(MARKETPLACE) && existsSync(join(REPO, "plugins"));
|
|
26
|
+
|
|
27
|
+
/** Claude Code's own agents: not in any marketplace, addressed by bare name. */
|
|
28
|
+
const BUILT_IN = new Set([
|
|
29
|
+
"Explore",
|
|
30
|
+
"Plan",
|
|
31
|
+
"general-purpose",
|
|
32
|
+
"statusline-setup",
|
|
33
|
+
"claude-code-guide",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Marketplace agents deliberately left to the preset's `fallback` grade. Each entry is a
|
|
38
|
+
* decision, not an omission: remove the line when the agent gets a tier. A NEW agent is not
|
|
39
|
+
* on this list, so adding one to the marketplace fails (b) until it is placed somewhere.
|
|
40
|
+
*/
|
|
41
|
+
const FALLBACK_BY_DESIGN = new Set([
|
|
42
|
+
"dev:developer",
|
|
43
|
+
"dev:devops",
|
|
44
|
+
"dev:frontend-developer",
|
|
45
|
+
"dev:researcher",
|
|
46
|
+
"dev:qa-engineer",
|
|
47
|
+
"madbench:bench",
|
|
48
|
+
"video-editing:timeline-builder",
|
|
49
|
+
"video-editing:transcriber",
|
|
50
|
+
"video-editing:video-processor",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
interface MarketplaceAgent {
|
|
54
|
+
id: string;
|
|
55
|
+
file: string;
|
|
56
|
+
frontmatterModel: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Every `plugins/<p>/agents/*.md` of every plugin the marketplace lists, by frontmatter name. */
|
|
60
|
+
function marketplaceAgents(): MarketplaceAgent[] {
|
|
61
|
+
const manifest = JSON.parse(readFileSync(MARKETPLACE, "utf8")) as {
|
|
62
|
+
plugins: { name: string; source: string }[];
|
|
63
|
+
};
|
|
64
|
+
const out: MarketplaceAgent[] = [];
|
|
65
|
+
for (const plugin of manifest.plugins) {
|
|
66
|
+
const agentsDir = join(REPO, plugin.source, "agents");
|
|
67
|
+
if (!existsSync(agentsDir)) continue;
|
|
68
|
+
for (const entry of readdirSync(agentsDir)) {
|
|
69
|
+
if (!entry.endsWith(".md")) continue;
|
|
70
|
+
const file = join(agentsDir, entry);
|
|
71
|
+
const text = readFileSync(file, "utf8");
|
|
72
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
73
|
+
if (!fm) continue;
|
|
74
|
+
const name = fm[1].match(/^name:\s*(.+?)\s*$/m)?.[1];
|
|
75
|
+
if (!name) continue;
|
|
76
|
+
const model = fm[1].match(/^model:\s*(.+?)\s*$/m)?.[1];
|
|
77
|
+
out.push({
|
|
78
|
+
id: `${plugin.name}:${name}`,
|
|
79
|
+
file: join(plugin.source, "agents", entry),
|
|
80
|
+
frontmatterModel: model,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe.skipIf(!inMagusSrc)(
|
|
88
|
+
"models-presets agent ids track the marketplace",
|
|
89
|
+
() => {
|
|
90
|
+
const agents = marketplaceAgents();
|
|
91
|
+
const ids = new Set(agents.map((a) => a.id));
|
|
92
|
+
const presetKeys = [...Object.keys(DEFAULT_AGENT_GRADES)];
|
|
93
|
+
|
|
94
|
+
test("reads a non-trivial marketplace", () => {
|
|
95
|
+
expect(agents.length).toBeGreaterThan(10);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("(a) every preset key names an existing agent or a built-in", () => {
|
|
99
|
+
const dead = presetKeys.filter((k) => !BUILT_IN.has(k) && !ids.has(k));
|
|
100
|
+
expect(dead).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("(b) every marketplace agent is classified somewhere", () => {
|
|
104
|
+
const classified = new Set<string>([
|
|
105
|
+
...presetKeys,
|
|
106
|
+
...VISION_AGENTS,
|
|
107
|
+
...FALLBACK_BY_DESIGN,
|
|
108
|
+
]);
|
|
109
|
+
const unplaced = agents
|
|
110
|
+
.filter((a) => !classified.has(a.id))
|
|
111
|
+
.map((a) => a.id);
|
|
112
|
+
expect(unplaced).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("FALLBACK_BY_DESIGN and VISION_AGENTS name agents that still exist", () => {
|
|
116
|
+
const stale = [...FALLBACK_BY_DESIGN, ...VISION_AGENTS].filter(
|
|
117
|
+
(k) => !ids.has(k),
|
|
118
|
+
);
|
|
119
|
+
expect(stale).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// A profile's magusPlugins is an install list, not documentation: resolver.ts expands
|
|
123
|
+
// each entry to `<name>@<marketplace>: latest`. A plugin rename moves under it silently,
|
|
124
|
+
// which is how 13.0.0's code-analysis -> code-search rename left six profiles installing
|
|
125
|
+
// a plugin the marketplace no longer lists. The agent checks above never read this array.
|
|
126
|
+
test("(d) every predefined profile names plugins the marketplace still lists", () => {
|
|
127
|
+
const marketplace = JSON.parse(readFileSync(MARKETPLACE, "utf8")) as {
|
|
128
|
+
plugins: { name: string }[];
|
|
129
|
+
};
|
|
130
|
+
const known = new Set(marketplace.plugins.map((p) => p.name));
|
|
131
|
+
const dead: string[] = [];
|
|
132
|
+
for (const profile of PREDEFINED_PROFILES) {
|
|
133
|
+
for (const entry of profile.magusPlugins ?? []) {
|
|
134
|
+
const name = entry.split("@")[0];
|
|
135
|
+
if (!known.has(name)) dead.push(`${profile.id}: ${entry}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
expect(dead).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
);
|
|
@@ -83,7 +83,7 @@ describe("resolveMarketplacePlugins — offline fallback", () => {
|
|
|
83
83
|
"magus",
|
|
84
84
|
buildLocalMarketplace("magus", [
|
|
85
85
|
{
|
|
86
|
-
name: "code-
|
|
86
|
+
name: "code-search",
|
|
87
87
|
version: "5.3.0",
|
|
88
88
|
description: "Codebase investigation tools",
|
|
89
89
|
category: "development",
|
|
@@ -108,12 +108,10 @@ describe("resolveMarketplacePlugins — offline fallback", () => {
|
|
|
108
108
|
|
|
109
109
|
expect(resolved.plugins).toHaveLength(2);
|
|
110
110
|
expect(resolved.plugins.map((p) => p.name).sort()).toEqual([
|
|
111
|
-
"code-
|
|
111
|
+
"code-search",
|
|
112
112
|
"dev",
|
|
113
113
|
]);
|
|
114
|
-
const codeAnalysis = resolved.plugins.find(
|
|
115
|
-
(p) => p.name === "code-analysis",
|
|
116
|
-
);
|
|
114
|
+
const codeAnalysis = resolved.plugins.find((p) => p.name === "code-search");
|
|
117
115
|
expect(codeAnalysis?.version).toBe("5.3.0");
|
|
118
116
|
expect(codeAnalysis?.description).toBe("Codebase investigation tools");
|
|
119
117
|
expect(codeAnalysis?.category).toBe("development");
|
|
@@ -91,9 +91,9 @@ describe("resolveExtends", () => {
|
|
|
91
91
|
extends: "growth-marketer",
|
|
92
92
|
};
|
|
93
93
|
const merged = resolveExtends(entry);
|
|
94
|
-
// image
|
|
94
|
+
// image/video-editing ship on the magus-marketing channel and
|
|
95
95
|
// pin it explicitly — they must NOT be rewritten to @magus.
|
|
96
|
-
expect(merged.plugins!["image
|
|
96
|
+
expect(merged.plugins!["image@magus-marketing"]).toBe("latest");
|
|
97
97
|
expect(merged.plugins!["video-editing@magus-marketing"]).toBe("latest");
|
|
98
98
|
expect(merged.plugins!["image-generate@magus"]).toBeUndefined();
|
|
99
99
|
// Bare names still default to @magus.
|
|
@@ -302,7 +302,7 @@ describe("marketplaces derived from plugin ids", () => {
|
|
|
302
302
|
name: "Mixed",
|
|
303
303
|
plugins: {
|
|
304
304
|
"dev@magus": "latest",
|
|
305
|
-
"image
|
|
305
|
+
"image@magus-marketing": "latest",
|
|
306
306
|
"feature-dev@claude-plugins-official": "latest",
|
|
307
307
|
},
|
|
308
308
|
},
|
|
@@ -40,19 +40,18 @@ export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
|
40
40
|
// what is true; both were on `smart` because "writes an important document" got confused
|
|
41
41
|
// with "makes an expensive judgement".
|
|
42
42
|
"dev:spec-writer": "normal",
|
|
43
|
-
"dev:
|
|
43
|
+
"dev:aggregator": "normal",
|
|
44
44
|
|
|
45
45
|
// Reading a lot is not analysing.
|
|
46
46
|
//
|
|
47
|
-
// `
|
|
47
|
+
// `analyze` is read-only: it locates implementations, traces a path and reports
|
|
48
48
|
// file:line. That is high-volume retrieval with light inference — the same shape as
|
|
49
49
|
// `Explore`, with more structure in the output. It sat on `smart` because "investigates"
|
|
50
50
|
// sounds like thinking; what it actually does is look.
|
|
51
|
-
"code-
|
|
51
|
+
"code-search:analyze": "cheap",
|
|
52
52
|
|
|
53
|
-
// Search
|
|
53
|
+
// Search and bookkeeping: bounded, mechanical, high-volume.
|
|
54
54
|
Explore: "cheap",
|
|
55
|
-
"dev:scribe": "cheap",
|
|
56
55
|
"dev:stack-detector": "cheap",
|
|
57
56
|
"dev:docs": "cheap",
|
|
58
57
|
"terminal:tui-navigator": "cheap",
|
|
@@ -69,7 +68,7 @@ export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
|
69
68
|
* answers to "what will this preset cost me", and the lump sum shows neither.
|
|
70
69
|
*
|
|
71
70
|
* Read out of `plugins/dev/commands/{dev,debug,investigate}.md` — agents only. Those files
|
|
72
|
-
* also name skills (`dev:context-detection`, `code-
|
|
71
|
+
* also name skills (`dev:context-detection`, `code-search:investigate`) which are not
|
|
73
72
|
* dispatched through the Agent tool and so are not routed at all.
|
|
74
73
|
*/
|
|
75
74
|
export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
@@ -79,7 +78,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
79
78
|
"dev:stack-detector",
|
|
80
79
|
"dev:architect",
|
|
81
80
|
"dev:developer",
|
|
82
|
-
"dev:
|
|
81
|
+
"dev:qa-engineer",
|
|
83
82
|
"dev:reviewer",
|
|
84
83
|
],
|
|
85
84
|
},
|
|
@@ -87,14 +86,14 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
87
86
|
name: "debug",
|
|
88
87
|
agents: [
|
|
89
88
|
"dev:stack-detector",
|
|
90
|
-
"code-
|
|
89
|
+
"code-search:analyze",
|
|
91
90
|
"dev:debugger",
|
|
92
91
|
"dev:developer",
|
|
93
92
|
],
|
|
94
93
|
},
|
|
95
94
|
{
|
|
96
95
|
name: "investigate",
|
|
97
|
-
agents: ["code-
|
|
96
|
+
agents: ["code-search:analyze", "dev:researcher"],
|
|
98
97
|
},
|
|
99
98
|
];
|
|
100
99
|
|
|
@@ -102,7 +101,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
102
101
|
* Agents whose model is a CAPABILITY choice, not a spend choice.
|
|
103
102
|
*
|
|
104
103
|
* The three tiers answer "how much model", and that is the wrong question for an agent that
|
|
105
|
-
* reads images. `designer:
|
|
104
|
+
* reads images. `designer:review` compares a rendered screen against a reference and
|
|
106
105
|
* `designer:ui` audits a screenshot for usability: what they need is the model that SEES
|
|
107
106
|
* best, which is not a point on a cheap-to-smart line and does not move when the preset
|
|
108
107
|
* changes.
|
|
@@ -112,7 +111,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
112
111
|
* preset's `fallback`, which is a defensible default and an honest one; pinning them to a
|
|
113
112
|
* model id guessed here is neither.
|
|
114
113
|
*/
|
|
115
|
-
export const VISION_AGENTS = ["designer:
|
|
114
|
+
export const VISION_AGENTS = ["designer:review", "designer:ui"] as const;
|
|
116
115
|
|
|
117
116
|
function preset(
|
|
118
117
|
name: string,
|
|
@@ -52,7 +52,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
52
52
|
magusPlugins: [
|
|
53
53
|
"setup",
|
|
54
54
|
"multimodel",
|
|
55
|
-
"code-
|
|
55
|
+
"code-search",
|
|
56
56
|
"dev",
|
|
57
57
|
"browser-use",
|
|
58
58
|
"designer",
|
|
@@ -90,7 +90,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
90
90
|
icon: "★",
|
|
91
91
|
magusPlugins: [
|
|
92
92
|
"dev",
|
|
93
|
-
"code-
|
|
93
|
+
"code-search",
|
|
94
94
|
"terminal",
|
|
95
95
|
"setup",
|
|
96
96
|
"designer",
|
|
@@ -129,7 +129,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
129
129
|
name: "Backend Forge",
|
|
130
130
|
description: "API development, debugging, code quality, data workflows",
|
|
131
131
|
icon: "★",
|
|
132
|
-
magusPlugins: ["dev", "code-
|
|
132
|
+
magusPlugins: ["dev", "code-search", "terminal", "setup", "multimodel"],
|
|
133
133
|
anthropicPlugins: [
|
|
134
134
|
"feature-dev",
|
|
135
135
|
"code-review",
|
|
@@ -159,7 +159,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
159
159
|
icon: "★",
|
|
160
160
|
magusPlugins: [
|
|
161
161
|
"dev",
|
|
162
|
-
"code-
|
|
162
|
+
"code-search",
|
|
163
163
|
"terminal",
|
|
164
164
|
"setup",
|
|
165
165
|
"multimodel",
|
|
@@ -194,11 +194,11 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
194
194
|
icon: "★",
|
|
195
195
|
magusPlugins: [
|
|
196
196
|
"dev",
|
|
197
|
-
"code-
|
|
197
|
+
"code-search",
|
|
198
198
|
"terminal",
|
|
199
199
|
"setup",
|
|
200
200
|
"browser-use",
|
|
201
|
-
"image
|
|
201
|
+
"image@magus-marketing",
|
|
202
202
|
"video-editing@magus-marketing",
|
|
203
203
|
],
|
|
204
204
|
anthropicPlugins: [
|
|
@@ -226,7 +226,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
226
226
|
description:
|
|
227
227
|
"Planning, code review, coordination, and broad repo visibility",
|
|
228
228
|
icon: "★",
|
|
229
|
-
magusPlugins: ["dev", "code-
|
|
229
|
+
magusPlugins: ["dev", "code-search", "terminal", "setup", "multimodel"],
|
|
230
230
|
anthropicPlugins: [
|
|
231
231
|
"code-review",
|
|
232
232
|
"pr-review-toolkit",
|
|
@@ -347,7 +347,7 @@ async function overlayRegistryVersions(
|
|
|
347
347
|
// binary, and `claude plugin install` writes it at no scope — so only
|
|
348
348
|
// claudeup's own past writes can be in there, and they go stale silently.
|
|
349
349
|
// Measured: user settings claimed browser-use 1.1.2 / terminal 4.0.2 /
|
|
350
|
-
// code-
|
|
350
|
+
// code-search 5.1.0 while the registry said 1.4.0 / 4.1.4 / 5.3.1, and
|
|
351
351
|
// none of the claimed versions had a cache directory. Keeping the stale
|
|
352
352
|
// value made every one of them read as a permanent pending update.
|
|
353
353
|
if (entries && entries.length > 0) delete merged[pluginId];
|
|
@@ -727,7 +727,7 @@ export function describeCommunityFailure(
|
|
|
727
727
|
// ─── Staleness: advisory, two-phase, never automatic ─────────────────────────
|
|
728
728
|
|
|
729
729
|
/**
|
|
730
|
-
* 24 hours, matching image
|
|
730
|
+
* 24 hours, matching the image plugin's staleness precedent in the root CLAUDE.md.
|
|
731
731
|
*
|
|
732
732
|
* The TTL is not only a "when to re-check" rule — it is also a "how long may we
|
|
733
733
|
* keep SAYING up-to-date" rule. A check from three days ago is evidence about
|