Skip to content

Commit 2b313b4

Browse files
committed
feat(claude-code): refresh marketplace + update plugin to latest version
When the hindsight-memory plugin is already installed, run: - claude plugin marketplace update <name> (refresh catalog) - claude plugin update <plugin>@<marketplace> (pull latest version) Also adds 17 unit tests covering: - Plugin install/update decision logic - Marketplace presence detection - allowedTools merge (idempotent, preserves existing entries) - Hindsight config persistence (prompt vs skip, cloud vs llmProvider)
1 parent 3d7a002 commit 2b313b4

16 files changed

Lines changed: 1735 additions & 5 deletions

File tree

src/cli.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -958,12 +958,18 @@ async function main() {
958958
p.log.warn(`Failed to install plugin: ${msg}`);
959959
}
960960
} else {
961-
// Update to latest
962-
p.log.info("Updating hindsight-memory plugin...");
961+
// Refresh marketplace catalog and update plugin to latest
962+
p.log.info("Updating hindsight-memory plugin to latest version...");
963963
try {
964-
execSync(`claude plugin update ${PLUGIN_NAME}@${MARKETPLACE_NAME}`, { stdio: "pipe" });
965-
p.log.success("Plugin up to date");
966-
} catch { /* ignore */ }
964+
execSync(`claude plugin marketplace update ${MARKETPLACE_NAME}`, { stdio: "pipe" });
965+
} catch { /* ignore — marketplace update is best-effort */ }
966+
try {
967+
execSync(`claude plugin update ${PLUGIN_NAME}@${MARKETPLACE_NAME}`, { stdio: "inherit" });
968+
p.log.success("Plugin updated to latest");
969+
} catch (err: any) {
970+
const msg = err?.stderr?.toString?.()?.trim() || err?.message || String(err);
971+
p.log.warn(`Failed to update plugin: ${msg}`);
972+
}
967973
}
968974

969975
// Step 3: Configure Hindsight connection in ~/.hindsight/claude-code.json

src/tests/cli.test.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,3 +625,174 @@ describe("harness validation", () => {
625625
expect(SUPPORTED_HARNESSES.includes("")).toBe(false);
626626
});
627627
});
628+
629+
// ── Claude Code plugin lifecycle decisions ──
630+
631+
describe("claude-code plugin install/update logic", () => {
632+
// Mirrors the decision logic in cli.ts:
633+
// - if plugin not in `claude plugin list` output → install
634+
// - else → run `claude plugin marketplace update` + `claude plugin update`
635+
function shouldInstall(pluginListOutput: string, pluginName: string): boolean {
636+
return !pluginListOutput.includes(pluginName);
637+
}
638+
639+
function shouldUpdate(pluginListOutput: string, pluginName: string): boolean {
640+
return pluginListOutput.includes(pluginName);
641+
}
642+
643+
it("installs when plugin not present", () => {
644+
const out = "Installed plugins:\n ❯ rust-analyzer-lsp@claude-plugins-official\n Version: 1.0.0";
645+
expect(shouldInstall(out, "hindsight-memory")).toBe(true);
646+
expect(shouldUpdate(out, "hindsight-memory")).toBe(false);
647+
});
648+
649+
it("updates when plugin already present", () => {
650+
const out = "Installed plugins:\n ❯ hindsight-memory@vectorize-io-hindsight\n Version: 0.5.0";
651+
expect(shouldInstall(out, "hindsight-memory")).toBe(false);
652+
expect(shouldUpdate(out, "hindsight-memory")).toBe(true);
653+
});
654+
655+
it("detects plugin regardless of installed scope", () => {
656+
const userScope = " ❯ hindsight-memory@vectorize-io-hindsight\n Scope: user";
657+
const localScope = " ❯ hindsight-memory@vectorize-io-hindsight\n Scope: local";
658+
expect(shouldUpdate(userScope, "hindsight-memory")).toBe(true);
659+
expect(shouldUpdate(localScope, "hindsight-memory")).toBe(true);
660+
});
661+
});
662+
663+
describe("claude-code marketplace detection", () => {
664+
// Mirrors the marketplace-add decision: if neither name nor repo is in the
665+
// `claude plugin marketplace list` output, we run `claude plugin marketplace add`.
666+
function hasMarketplace(out: string, name: string, repo: string): boolean {
667+
return out.includes(name) || out.includes(repo);
668+
}
669+
670+
const MARKETPLACE_NAME = "vectorize-io-hindsight";
671+
const MARKETPLACE_REPO = "vectorize-io/hindsight";
672+
673+
it("detects when marketplace already added by name", () => {
674+
const out = "Configured marketplaces:\n vectorize-io-hindsight (github: vectorize-io/hindsight)";
675+
expect(hasMarketplace(out, MARKETPLACE_NAME, MARKETPLACE_REPO)).toBe(true);
676+
});
677+
678+
it("detects when marketplace already added by repo", () => {
679+
const out = "Configured marketplaces:\n some-other-name (github: vectorize-io/hindsight)";
680+
expect(hasMarketplace(out, MARKETPLACE_NAME, MARKETPLACE_REPO)).toBe(true);
681+
});
682+
683+
it("returns false when marketplace not added", () => {
684+
const out = "Configured marketplaces:\n claude-plugins-official";
685+
expect(hasMarketplace(out, MARKETPLACE_NAME, MARKETPLACE_REPO)).toBe(false);
686+
});
687+
688+
it("returns false on empty marketplace list", () => {
689+
expect(hasMarketplace("", MARKETPLACE_NAME, MARKETPLACE_REPO)).toBe(false);
690+
});
691+
});
692+
693+
describe("claude-code allowed-tools merge", () => {
694+
// Mirrors the auto-approve logic that merges entries into ~/.claude/settings.json's allowedTools.
695+
function mergeAllowed(existing: string[], toAdd: string[]): { merged: string[]; updated: boolean } {
696+
const merged = [...existing];
697+
let updated = false;
698+
for (const tool of toAdd) {
699+
if (!merged.includes(tool)) {
700+
merged.push(tool);
701+
updated = true;
702+
}
703+
}
704+
return { merged, updated };
705+
}
706+
707+
const HINDSIGHT_TOOLS = [
708+
"mcp__hindsight__*",
709+
"Skill(hindsight-memory:create-agent)",
710+
"Bash(ls ~/.self-driving-agents/*)",
711+
"Bash(cat ~/.self-driving-agents/*)",
712+
];
713+
714+
it("adds all tools to empty allowedTools", () => {
715+
const { merged, updated } = mergeAllowed([], HINDSIGHT_TOOLS);
716+
expect(updated).toBe(true);
717+
expect(merged).toEqual(HINDSIGHT_TOOLS);
718+
});
719+
720+
it("preserves existing entries", () => {
721+
const { merged } = mergeAllowed(["Bash(npm *)", "Read"], HINDSIGHT_TOOLS);
722+
expect(merged).toContain("Bash(npm *)");
723+
expect(merged).toContain("Read");
724+
expect(merged).toContain("mcp__hindsight__*");
725+
});
726+
727+
it("does not duplicate when already present", () => {
728+
const existing = [...HINDSIGHT_TOOLS];
729+
const { merged, updated } = mergeAllowed(existing, HINDSIGHT_TOOLS);
730+
expect(updated).toBe(false);
731+
expect(merged).toHaveLength(HINDSIGHT_TOOLS.length);
732+
});
733+
734+
it("only adds missing entries", () => {
735+
const existing = ["mcp__hindsight__*"];
736+
const { merged, updated } = mergeAllowed(existing, HINDSIGHT_TOOLS);
737+
expect(updated).toBe(true);
738+
expect(merged).toHaveLength(HINDSIGHT_TOOLS.length);
739+
});
740+
});
741+
742+
describe("claude-code Hindsight config persistence", () => {
743+
// Mirrors the config-write logic: if existing config has a connection
744+
// (hindsightApiUrl or llmProvider), don't prompt; otherwise prompt.
745+
function shouldPromptConnection(config: Record<string, any>): boolean {
746+
return !config.hindsightApiUrl && !config.llmProvider;
747+
}
748+
749+
function applyClaudeConfig(
750+
existing: Record<string, any>,
751+
prompted: { apiUrl: string; apiToken?: string }
752+
): Record<string, any> {
753+
return {
754+
...existing,
755+
hindsightApiUrl: prompted.apiUrl,
756+
hindsightApiToken: prompted.apiToken,
757+
enableKnowledgeTools: true,
758+
};
759+
}
760+
761+
it("prompts on first install (empty config)", () => {
762+
expect(shouldPromptConnection({})).toBe(true);
763+
});
764+
765+
it("skips prompt when hindsightApiUrl already set", () => {
766+
expect(shouldPromptConnection({ hindsightApiUrl: "https://api.example.com" })).toBe(false);
767+
});
768+
769+
it("skips prompt when llmProvider already set (local daemon mode)", () => {
770+
expect(shouldPromptConnection({ llmProvider: "openai" })).toBe(false);
771+
});
772+
773+
it("writes Cloud connection on first install", () => {
774+
const result = applyClaudeConfig({}, {
775+
apiUrl: "https://api.hindsight.vectorize.io",
776+
apiToken: "hsk_abc",
777+
});
778+
expect(result.hindsightApiUrl).toBe("https://api.hindsight.vectorize.io");
779+
expect(result.hindsightApiToken).toBe("hsk_abc");
780+
expect(result.enableKnowledgeTools).toBe(true);
781+
});
782+
783+
it("preserves other settings when applying connection", () => {
784+
const existing = { debug: true, retainEveryNTurns: 1 };
785+
const result = applyClaudeConfig(existing, { apiUrl: "https://x.com", apiToken: "t" });
786+
expect(result.debug).toBe(true);
787+
expect(result.retainEveryNTurns).toBe(1);
788+
expect(result.hindsightApiUrl).toBe("https://x.com");
789+
});
790+
791+
it("always sets enableKnowledgeTools=true", () => {
792+
const result = applyClaudeConfig(
793+
{ enableKnowledgeTools: false },
794+
{ apiUrl: "https://x.com", apiToken: "t" }
795+
);
796+
expect(result.enableKnowledgeTools).toBe(true);
797+
});
798+
});

0 commit comments

Comments
 (0)