@takara-ai/miru-code 1.6.0 → 1.7.3

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.
@@ -9,6 +9,52 @@ function copilotHooksPath(home: string): string {
9
9
  return join(home, ".copilot", "hooks", "miru-search.json");
10
10
  }
11
11
 
12
+ /** Shared Copilot / VS Code / Visual Studio config root (MCP, hooks, agents). */
13
+ export function copilotHomeDir(home: string): string {
14
+ return join(home, ".copilot");
15
+ }
16
+
17
+ /**
18
+ * Vendors that do not scan `~/.agents/skills/` and need a native skill dir
19
+ * (Caveman + STE).
20
+ */
21
+ export type NativeSkillVendor = "claude" | "kiro";
22
+
23
+ /** @deprecated Use NativeSkillVendor. */
24
+ export type NativeCavemanVendor = NativeSkillVendor;
25
+
26
+ function skillMd(home: string, rootDir: string, skillName: string): string {
27
+ return join(home, rootDir, "skills", skillName, "SKILL.md");
28
+ }
29
+
30
+ function skillDir(home: string, rootDir: string, skillName: string): string {
31
+ return join(home, rootDir, "skills", skillName);
32
+ }
33
+
34
+ /**
35
+ * Cross-agent Agent Skills root (`~/.agents/skills/…`).
36
+ * Used by Cursor, Gemini, OpenCode, Codex, Windsurf, and Copilot-family IDEs.
37
+ * Claude Code and Kiro still use vendor-native skill dirs.
38
+ */
39
+ export function agentsCavemanSkillPath(home: string): string {
40
+ return skillMd(home, ".agents", "caveman");
41
+ }
42
+
43
+ /** Claude Code / Kiro native Caveman skill path. */
44
+ export function nativeCavemanSkillPath(home: string, vendor: NativeSkillVendor): string {
45
+ return skillMd(home, `.${vendor}`, "caveman");
46
+ }
47
+
48
+ /** Shared STE skill directory under `~/.agents/skills/ste`. */
49
+ export function agentsSteSkillDir(home: string): string {
50
+ return skillDir(home, ".agents", "ste");
51
+ }
52
+
53
+ /** Claude Code / Kiro native STE skill directory. */
54
+ export function nativeSteSkillDir(home: string, vendor: NativeSkillVendor): string {
55
+ return skillDir(home, `.${vendor}`, "ste");
56
+ }
57
+
12
58
  function kiroHooksPath(home: string): string {
13
59
  return join(home, ".kiro", "settings", "hooks.json");
14
60
  }
@@ -17,10 +63,14 @@ function windsurfHooksPath(home: string): string {
17
63
  return join(home, ".codeium", "windsurf", "hooks.json");
18
64
  }
19
65
 
20
- function opencodePluginPath(home: string): string {
66
+ /** OpenCode config root (`$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`). */
67
+ export function opencodeConfigDir(home: string): string {
21
68
  const xdg = process.env.XDG_CONFIG_HOME;
22
- const base = xdg ? join(xdg, "opencode") : join(home, ".config", "opencode");
23
- return join(base, "plugins", "miru-search-guard.ts");
69
+ return xdg ? join(xdg, "opencode") : join(home, ".config", "opencode");
70
+ }
71
+
72
+ function opencodePluginPath(home: string): string {
73
+ return join(opencodeConfigDir(home), "plugins", "miru-search-guard.ts");
24
74
  }
25
75
 
26
76
  export type InstallAction =
@@ -92,13 +142,19 @@ export interface AgentTarget {
92
142
  hooksFormat: HooksFormat | null;
93
143
  subagentPath: string | null;
94
144
  subagentId: AgentId | null;
95
- /** On-demand Caveman Agent Skill (`…/skills/caveman/SKILL.md`), or null if unsupported. */
145
+ /**
146
+ * On-demand Caveman Agent Skill (`…/skills/caveman/SKILL.md`), or null if
147
+ * unsupported.
148
+ */
96
149
  cavemanSkillPath: string | null;
150
+ /**
151
+ * On-demand STE skill directory (`…/skills/ste/`), or null if unsupported.
152
+ */
153
+ steSkillDir: string | null;
97
154
  }
98
155
 
99
156
  export function opencodeMcpPath(): string {
100
- const xdg = process.env.XDG_CONFIG_HOME;
101
- const base = xdg ? join(xdg, "opencode") : join(HOME, ".config", "opencode");
157
+ const base = opencodeConfigDir(HOME);
102
158
  const jsonc = join(base, "opencode.jsonc");
103
159
  const json = join(base, "opencode.json");
104
160
  if (existsSync(jsonc)) {
@@ -165,6 +221,9 @@ function jsonMcp(path: string, key: string, entry: Record<string, unknown>): Mcp
165
221
  return { path, key, memberKey: "miru", entry, format: "json" };
166
222
  }
167
223
 
224
+ const SHARED_CAVEMAN_SKILL = agentsCavemanSkillPath(HOME);
225
+ const SHARED_STE_SKILL_DIR = agentsSteSkillDir(HOME);
226
+
168
227
  export const AGENT_TARGETS: AgentTarget[] = [
169
228
  {
170
229
  id: "claude",
@@ -178,7 +237,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
178
237
  hooksFormat: "claude",
179
238
  subagentPath: join(HOME, ".claude", "agents", "miru-code.md"),
180
239
  subagentId: "claude",
181
- cavemanSkillPath: join(HOME, ".claude", "skills", "caveman", "SKILL.md"),
240
+ cavemanSkillPath: nativeCavemanSkillPath(HOME, "claude"),
241
+ steSkillDir: nativeSteSkillDir(HOME, "claude"),
182
242
  },
183
243
  {
184
244
  id: "cursor",
@@ -192,7 +252,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
192
252
  hooksFormat: "cursor",
193
253
  subagentPath: join(HOME, ".cursor", "agents", "miru-code.md"),
194
254
  subagentId: "cursor",
195
- cavemanSkillPath: join(HOME, ".cursor", "skills", "caveman", "SKILL.md"),
255
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
256
+ steSkillDir: SHARED_STE_SKILL_DIR,
196
257
  },
197
258
  {
198
259
  id: "gemini",
@@ -206,7 +267,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
206
267
  hooksFormat: "gemini",
207
268
  subagentPath: join(HOME, ".gemini", "agents", "miru-code.md"),
208
269
  subagentId: "gemini",
209
- cavemanSkillPath: null,
270
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
271
+ steSkillDir: SHARED_STE_SKILL_DIR,
210
272
  },
211
273
  {
212
274
  id: "kiro",
@@ -220,35 +282,42 @@ export const AGENT_TARGETS: AgentTarget[] = [
220
282
  hooksFormat: "kiro",
221
283
  subagentPath: join(HOME, ".kiro", "agents", "miru-code.md"),
222
284
  subagentId: "kiro",
223
- cavemanSkillPath: null,
285
+ cavemanSkillPath: nativeCavemanSkillPath(HOME, "kiro"),
286
+ steSkillDir: nativeSteSkillDir(HOME, "kiro"),
224
287
  },
225
288
  {
226
289
  id: "opencode",
227
290
  displayName: "OpenCode",
228
291
  binary: "opencode",
229
- configDir: join(HOME, ".config", "opencode"),
292
+ configDir: opencodeConfigDir(HOME),
230
293
  mcp: jsonMcp(opencodeMcpPath(), "mcp", OPENCODE_SERVER_CONFIG),
231
- instructionsPath: join(HOME, ".config", "opencode", "AGENTS.md"),
294
+ instructionsPath: join(opencodeConfigDir(HOME), "AGENTS.md"),
232
295
  cursorRulesPath: null,
233
296
  hooksPath: opencodePluginPath(HOME),
234
297
  hooksFormat: "opencode",
235
- subagentPath: join(HOME, ".config", "opencode", "agents", "miru-code.md"),
298
+ subagentPath: join(opencodeConfigDir(HOME), "agents", "miru-code.md"),
236
299
  subagentId: "opencode",
237
- cavemanSkillPath: null,
300
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
301
+ steSkillDir: SHARED_STE_SKILL_DIR,
238
302
  },
239
303
  {
240
304
  id: "copilot",
241
305
  displayName: "GitHub Copilot",
242
306
  binary: null,
243
307
  configDir: join(HOME, ".config", "github-copilot"),
244
- mcp: jsonMcp(join(HOME, ".copilot", "mcp-config.json"), "mcpServers", BARE_STDIO_SERVER_CONFIG),
308
+ mcp: jsonMcp(
309
+ join(copilotHomeDir(HOME), "mcp-config.json"),
310
+ "mcpServers",
311
+ BARE_STDIO_SERVER_CONFIG,
312
+ ),
245
313
  instructionsPath: null,
246
314
  cursorRulesPath: null,
247
315
  hooksPath: copilotHooksPath(HOME),
248
316
  hooksFormat: "vscode",
249
- subagentPath: join(HOME, ".copilot", "agents", "miru-code.agent.md"),
317
+ subagentPath: join(copilotHomeDir(HOME), "agents", "miru-code.agent.md"),
250
318
  subagentId: "copilot",
251
- cavemanSkillPath: null,
319
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
320
+ steSkillDir: SHARED_STE_SKILL_DIR,
252
321
  },
253
322
  {
254
323
  id: "codex",
@@ -268,7 +337,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
268
337
  hooksFormat: "claude",
269
338
  subagentPath: null,
270
339
  subagentId: null,
271
- cavemanSkillPath: null,
340
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
341
+ steSkillDir: SHARED_STE_SKILL_DIR,
272
342
  },
273
343
  {
274
344
  id: "vscode",
@@ -282,7 +352,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
282
352
  hooksFormat: "vscode",
283
353
  subagentPath: null,
284
354
  subagentId: null,
285
- cavemanSkillPath: null,
355
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
356
+ steSkillDir: SHARED_STE_SKILL_DIR,
286
357
  },
287
358
  {
288
359
  id: "windsurf",
@@ -296,7 +367,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
296
367
  hooksFormat: "windsurf",
297
368
  subagentPath: null,
298
369
  subagentId: null,
299
- cavemanSkillPath: null,
370
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
371
+ steSkillDir: SHARED_STE_SKILL_DIR,
300
372
  },
301
373
  {
302
374
  id: "visualstudio",
@@ -310,7 +382,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
310
382
  hooksFormat: "vscode",
311
383
  subagentPath: null,
312
384
  subagentId: null,
313
- cavemanSkillPath: null,
385
+ cavemanSkillPath: SHARED_CAVEMAN_SKILL,
386
+ steSkillDir: SHARED_STE_SKILL_DIR,
314
387
  },
315
388
  ];
316
389
 
@@ -337,6 +410,10 @@ export async function isAgentDetected(agent: AgentTarget): Promise<boolean> {
337
410
  }
338
411
  return false;
339
412
  }
413
+ // Copilot-specific artifacts only — ~/.copilot alone is shared with VS Code / VS.
414
+ if (agent.id === "copilot") {
415
+ return isCopilotInstalled(HOME);
416
+ }
340
417
  if (agent.binary && (await commandOnPath(agent.binary))) {
341
418
  return true;
342
419
  }
@@ -345,3 +422,21 @@ export async function isAgentDetected(agent: AgentTarget): Promise<boolean> {
345
422
  }
346
423
  return false;
347
424
  }
425
+
426
+ /**
427
+ * True when Copilot-specific config exists. Does not treat shared
428
+ * `~/.copilot` hooks (also used by VS Code / Visual Studio) as enough.
429
+ */
430
+ export function isCopilotInstalled(home: string = HOME): boolean {
431
+ if (existsSync(join(home, ".config", "github-copilot"))) {
432
+ return true;
433
+ }
434
+ const copilot = copilotHomeDir(home);
435
+ if (existsSync(join(copilot, "mcp-config.json"))) {
436
+ return true;
437
+ }
438
+ if (existsSync(join(copilot, "agents"))) {
439
+ return true;
440
+ }
441
+ return false;
442
+ }
@@ -0,0 +1,51 @@
1
+ import type { AgentTarget, InstallAction } from "./agents.ts";
2
+ import {
3
+ ensureSharedSkillOwnersOnInstall,
4
+ isSharedSkillPath,
5
+ removeSkillMdAndOwners,
6
+ resolveSharedSkillUninstall,
7
+ type SharedSkillCtx,
8
+ type SharedSkillUninstallDecision,
9
+ SKILL_OWNERS_FILE,
10
+ type SkillPathOf,
11
+ } from "./shared-skill.ts";
12
+
13
+ /** @deprecated Prefer SKILL_OWNERS_FILE — same sidecar name for all shared skills. */
14
+ export const CAVEMAN_OWNERS_FILE = SKILL_OWNERS_FILE;
15
+
16
+ /** Context for shared `~/.agents/skills` Caveman install/uninstall. */
17
+ export type CavemanSharedCtx = SharedSkillCtx;
18
+
19
+ export type SharedCavemanUninstallDecision = SharedSkillUninstallDecision;
20
+
21
+ const cavemanPathOf: SkillPathOf = (agent) => agent.cavemanSkillPath;
22
+
23
+ export function isSharedCavemanPath(path: string, allAgents: AgentTarget[]): boolean {
24
+ return isSharedSkillPath(path, allAgents, cavemanPathOf);
25
+ }
26
+
27
+ export async function ensureSharedCavemanOwnersOnInstall(
28
+ skillDir: string,
29
+ skillPath: string,
30
+ agent: AgentTarget,
31
+ ctx: CavemanSharedCtx,
32
+ ): Promise<void> {
33
+ return ensureSharedSkillOwnersOnInstall(skillDir, skillPath, agent, ctx, cavemanPathOf);
34
+ }
35
+
36
+ export async function resolveSharedCavemanUninstall(
37
+ path: string,
38
+ skillDir: string,
39
+ agent: AgentTarget,
40
+ ctx: CavemanSharedCtx,
41
+ ): Promise<SharedCavemanUninstallDecision> {
42
+ return resolveSharedSkillUninstall(path, skillDir, agent, ctx, cavemanPathOf);
43
+ }
44
+
45
+ /** Remove SKILL.md and owners sidecar; leave sibling skill folders untouched. */
46
+ export async function removeCavemanSkillFiles(
47
+ path: string,
48
+ skillDir: string,
49
+ ): Promise<Extract<InstallAction, "removed" | "not-found">> {
50
+ return removeSkillMdAndOwners(path, skillDir);
51
+ }
@@ -616,3 +616,76 @@ export async function removeTomlBlock(path: string): Promise<InstallAction> {
616
616
  await Bun.write(path, `${remaining}\n`);
617
617
  return "removed";
618
618
  }
619
+
620
+ /**
621
+ * Ensure Codex loads Agent Skills: `[features] skills = true` in config.toml.
622
+ * Does not disable the flag on uninstall (other skills may still need it).
623
+ */
624
+ export function codexSkillsFeatureEnabled(text: string): boolean {
625
+ const lines = text.split("\n");
626
+ let inFeatures = false;
627
+ for (const line of lines) {
628
+ const tableKey = line.split("#")[0]?.trim() ?? "";
629
+ if (tableKey.startsWith("[") && tableKey.endsWith("]")) {
630
+ inFeatures = tableKey === "[features]";
631
+ continue;
632
+ }
633
+ if (!inFeatures) {
634
+ continue;
635
+ }
636
+ const value = line.split("#")[0]?.match(/^\s*skills\s*=\s*(true|false)\s*$/i)?.[1];
637
+ if (value?.toLowerCase() === "true") {
638
+ return true;
639
+ }
640
+ }
641
+ return false;
642
+ }
643
+
644
+ export async function ensureCodexSkillsFeature(path: string): Promise<InstallAction> {
645
+ const existed = await Bun.file(path).exists();
646
+ const existing = existed ? await Bun.file(path).text() : "";
647
+ if (codexSkillsFeatureEnabled(existing)) {
648
+ return "unchanged";
649
+ }
650
+ const lines = existing.length > 0 ? existing.split("\n") : [];
651
+
652
+ let featuresIdx = -1;
653
+ let skillsIdx = -1;
654
+ let inFeatures = false;
655
+
656
+ for (let i = 0; i < lines.length; i++) {
657
+ const tableKey = (lines[i] ?? "").split("#")[0]?.trim() ?? "";
658
+ if (tableKey.startsWith("[") && tableKey.endsWith("]")) {
659
+ if (tableKey === "[features]") {
660
+ inFeatures = true;
661
+ featuresIdx = i;
662
+ continue;
663
+ }
664
+ inFeatures = false;
665
+ continue;
666
+ }
667
+ if (inFeatures && /^\s*skills\s*=/.test(lines[i] ?? "")) {
668
+ skillsIdx = i;
669
+ }
670
+ }
671
+
672
+ if (featuresIdx >= 0 && skillsIdx >= 0) {
673
+ lines[skillsIdx] = "skills = true";
674
+ const next = `${lines.join("\n").replace(/\n+$/, "")}\n`;
675
+ await Bun.write(path, next);
676
+ return "updated";
677
+ }
678
+
679
+ if (featuresIdx >= 0) {
680
+ lines.splice(featuresIdx + 1, 0, "skills = true");
681
+ const next = `${lines.join("\n").replace(/\n+$/, "")}\n`;
682
+ await Bun.write(path, next);
683
+ return "updated";
684
+ }
685
+
686
+ const block = "[features]\nskills = true\n";
687
+ const base = existing.replace(/\n+$/, "");
688
+ const next = base.length > 0 ? `${base}\n\n${block}` : block;
689
+ await Bun.write(path, next.endsWith("\n") ? next : `${next}\n`);
690
+ return existed ? "updated" : "created";
691
+ }
@@ -0,0 +1,8 @@
1
+ /** Agents that share `~/.copilot` hooks. Caveman uses `~/.agents/skills`. */
2
+ export const COPILOT_FAMILY_IDS = ["copilot", "vscode", "visualstudio"] as const;
3
+
4
+ export type CopilotFamilyId = (typeof COPILOT_FAMILY_IDS)[number];
5
+
6
+ export function isCopilotFamilyId(id: string): id is CopilotFamilyId {
7
+ return (COPILOT_FAMILY_IDS as readonly string[]).includes(id);
8
+ }
@@ -2,11 +2,11 @@ import { unlink } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import type { HooksFormat, InstallAction } from "../agents.ts";
4
4
  import { stripJsonComments } from "../config.ts";
5
+ import { isCopilotFamilyId } from "../copilot-family.ts";
5
6
 
6
7
  const HOOK_GUARD_MARKER = "hook-guard";
7
8
  const MIRU_HOOK_MARKER = "miru-search";
8
9
  const VSCODE_OWNERS_KEY = "miruOwners";
9
- const SHARED_VSCODE_HOOK_OWNERS = ["copilot", "vscode", "visualstudio"] as const;
10
10
 
11
11
  const MATCHERS = {
12
12
  cursor: "Grep|Glob|Shell|SemanticSearch",
@@ -346,22 +346,11 @@ export async function mergeVscodeHooks(path: string, owner?: string): Promise<In
346
346
 
347
347
  const currentOwners = Array.isArray(parsed[VSCODE_OWNERS_KEY])
348
348
  ? (parsed[VSCODE_OWNERS_KEY] as unknown[]).filter(
349
- (item): item is string =>
350
- typeof item === "string" &&
351
- SHARED_VSCODE_HOOK_OWNERS.includes(item as (typeof SHARED_VSCODE_HOOK_OWNERS)[number]),
349
+ (item): item is string => typeof item === "string" && isCopilotFamilyId(item),
352
350
  )
353
351
  : [];
354
352
  const nextOwners = owner
355
- ? Array.from(
356
- new Set([
357
- ...currentOwners,
358
- ...(SHARED_VSCODE_HOOK_OWNERS.includes(
359
- owner as (typeof SHARED_VSCODE_HOOK_OWNERS)[number],
360
- )
361
- ? [owner]
362
- : []),
363
- ]),
364
- )
353
+ ? Array.from(new Set([...currentOwners, ...(isCopilotFamilyId(owner) ? [owner] : [])]))
365
354
  : currentOwners;
366
355
 
367
356
  const next = {