@takara-ai/miru-code 1.5.3 → 1.7.2
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/README.md +102 -25
- package/docs/miru-workflow-diagram.svg +47 -0
- package/docs/self-hosted-sagemaker.md +1 -1
- package/package.json +3 -1
- package/scripts/install-git-hooks.ts +29 -0
- package/src/auth/device.ts +306 -0
- package/src/auth/errors.ts +24 -0
- package/src/auth/providers.ts +115 -0
- package/src/auth/types.ts +65 -0
- package/src/cli.ts +59 -8
- package/src/credentials.ts +173 -65
- package/src/embeddings/openai.ts +3 -2
- package/src/env.ts +6 -2
- package/src/help.ts +29 -13
- package/src/installer/agents.ts +118 -16
- package/src/installer/caveman-shared.ts +51 -0
- package/src/installer/config.ts +73 -0
- package/src/installer/copilot-family.ts +8 -0
- package/src/installer/hooks/install.ts +3 -14
- package/src/installer/installer.ts +332 -11
- package/src/installer/prompt.ts +15 -8
- package/src/installer/search-policy.ts +3 -2
- package/src/installer/shared-skill.ts +200 -0
- package/src/installer/style-packs/caveman.ts +118 -0
- package/src/installer/style-packs/ste/skill.ts +218 -0
- package/src/mcp/auth-tool.ts +161 -0
- package/src/mcp/index-cache.ts +2 -1
- package/src/mcp/server.ts +7 -4
- package/src/prompt.ts +8 -5
- package/src/setup.ts +100 -69
- package/src/spinner.ts +33 -6
- package/tokenizer/tokenizer.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { unlink } from "node:fs/promises";
|
|
1
|
+
import { mkdir, rmdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
2
3
|
import { loadAgentTemplate } from "../agents.ts";
|
|
3
4
|
import { clearBenchmarkHistory } from "../benchmark/history.ts";
|
|
4
5
|
import { brandTitle, dim, divider, green, hint, info, success, writeStdout } from "../cli-ui.ts";
|
|
@@ -13,6 +14,14 @@ import {
|
|
|
13
14
|
} from "./agents.ts";
|
|
14
15
|
import { withPreservedBenchmarkFlag } from "./benchmark-mode.ts";
|
|
15
16
|
import {
|
|
17
|
+
ensureSharedCavemanOwnersOnInstall,
|
|
18
|
+
isSharedCavemanPath,
|
|
19
|
+
removeCavemanSkillFiles,
|
|
20
|
+
resolveSharedCavemanUninstall,
|
|
21
|
+
} from "./caveman-shared.ts";
|
|
22
|
+
import {
|
|
23
|
+
codexSkillsFeatureEnabled,
|
|
24
|
+
ensureCodexSkillsFeature,
|
|
16
25
|
mergeJsonMember,
|
|
17
26
|
mergeTomlBlock,
|
|
18
27
|
removeJsonMember,
|
|
@@ -24,13 +33,44 @@ import {
|
|
|
24
33
|
import { mergeHooks, removeHooks } from "./hooks/install.ts";
|
|
25
34
|
import { promptConfirm, promptMultiSelect, requireInteractiveTerminal } from "./prompt.ts";
|
|
26
35
|
import { CURSOR_RULES_MDC } from "./search-policy.ts";
|
|
36
|
+
import {
|
|
37
|
+
ensureSharedSkillOwnersOnInstall,
|
|
38
|
+
isSharedSkillPath,
|
|
39
|
+
removeSkillMdAndOwners,
|
|
40
|
+
resolveSharedSkillUninstall,
|
|
41
|
+
type SkillPathOf,
|
|
42
|
+
} from "./shared-skill.ts";
|
|
43
|
+
import { CAVEMAN_SKILL_MD } from "./style-packs/caveman.ts";
|
|
44
|
+
import { STE_REFERENCE_FILES, STE_SKILL_MD } from "./style-packs/ste/skill.ts";
|
|
45
|
+
|
|
46
|
+
const steSkillPathOf: SkillPathOf = (agent) =>
|
|
47
|
+
agent.steSkillDir ? join(agent.steSkillDir, "SKILL.md") : null;
|
|
27
48
|
|
|
28
49
|
export interface WriteResult {
|
|
29
50
|
path: string;
|
|
30
51
|
action: InstallAction;
|
|
52
|
+
note?: string;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
export type IntegrationId =
|
|
55
|
+
export type IntegrationId =
|
|
56
|
+
| "mcp"
|
|
57
|
+
| "instructions"
|
|
58
|
+
| "subagent"
|
|
59
|
+
| "hooks"
|
|
60
|
+
| "rules"
|
|
61
|
+
| "caveman"
|
|
62
|
+
| "ste";
|
|
63
|
+
|
|
64
|
+
/** Shared context for one install/uninstall pass (path dedupe + shared-skill keep). */
|
|
65
|
+
export interface ApplyCtx {
|
|
66
|
+
selectedAgents: AgentTarget[];
|
|
67
|
+
cavemanSeenPaths: Set<string>;
|
|
68
|
+
steSeenPaths: Set<string>;
|
|
69
|
+
/** Defaults to AGENT_TARGETS; override in tests. */
|
|
70
|
+
allAgents?: AgentTarget[];
|
|
71
|
+
/** Defaults to isAgentDetected; override in tests. */
|
|
72
|
+
isDetected?: (agent: AgentTarget) => Promise<boolean>;
|
|
73
|
+
}
|
|
34
74
|
|
|
35
75
|
interface Integration {
|
|
36
76
|
id: IntegrationId;
|
|
@@ -39,7 +79,7 @@ interface Integration {
|
|
|
39
79
|
experimental?: boolean;
|
|
40
80
|
defaultChecked?: boolean;
|
|
41
81
|
planPath: (agent: AgentTarget) => string | null;
|
|
42
|
-
apply: (agent: AgentTarget, mode: InstallMode) => Promise<WriteResult | null>;
|
|
82
|
+
apply: (agent: AgentTarget, mode: InstallMode, ctx?: ApplyCtx) => Promise<WriteResult | null>;
|
|
43
83
|
}
|
|
44
84
|
|
|
45
85
|
const ACTION_DETAIL: Partial<Record<InstallAction, string>> = {
|
|
@@ -183,6 +223,202 @@ async function applySubagent(agent: AgentTarget, mode: InstallMode): Promise<Wri
|
|
|
183
223
|
}
|
|
184
224
|
}
|
|
185
225
|
|
|
226
|
+
async function withCodexSkillsFeature(
|
|
227
|
+
agent: AgentTarget,
|
|
228
|
+
result: WriteResult,
|
|
229
|
+
): Promise<WriteResult> {
|
|
230
|
+
if (agent.id !== "codex" || !agent.mcp?.path) {
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
const featureAction = await ensureCodexSkillsFeature(agent.mcp.path);
|
|
234
|
+
if (featureAction === "unchanged") {
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
const skillsNote = "enabled [features] skills = true in config.toml";
|
|
238
|
+
if (result.action === "unchanged") {
|
|
239
|
+
return {
|
|
240
|
+
...result,
|
|
241
|
+
action: "updated",
|
|
242
|
+
note: result.note ? `${result.note}; ${skillsNote}` : skillsNote,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
...result,
|
|
247
|
+
note: result.note
|
|
248
|
+
? `${result.note}; also sets [features] skills = true in config.toml`
|
|
249
|
+
: "also sets [features] skills = true in config.toml",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function applyCaveman(
|
|
254
|
+
agent: AgentTarget,
|
|
255
|
+
mode: InstallMode,
|
|
256
|
+
ctx: ApplyCtx = {
|
|
257
|
+
selectedAgents: [agent],
|
|
258
|
+
cavemanSeenPaths: new Set(),
|
|
259
|
+
steSeenPaths: new Set(),
|
|
260
|
+
},
|
|
261
|
+
): Promise<WriteResult | null> {
|
|
262
|
+
const path = agent.cavemanSkillPath;
|
|
263
|
+
if (!path) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const skillDir = dirname(path);
|
|
268
|
+
const allAgents = ctx.allAgents ?? AGENT_TARGETS;
|
|
269
|
+
const shared = isSharedCavemanPath(path, allAgents);
|
|
270
|
+
const sharedCtx = {
|
|
271
|
+
selectedAgents: ctx.selectedAgents,
|
|
272
|
+
allAgents: ctx.allAgents,
|
|
273
|
+
isDetected: ctx.isDetected,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (mode === "uninstall") {
|
|
277
|
+
if (shared) {
|
|
278
|
+
const decision = await resolveSharedCavemanUninstall(path, skillDir, agent, sharedCtx);
|
|
279
|
+
if (decision.kind === "defer" || decision.kind === "keep") {
|
|
280
|
+
return { path, action: "unchanged", note: decision.note };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { path, action: await removeCavemanSkillFiles(path, skillDir) };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (shared) {
|
|
287
|
+
await ensureSharedCavemanOwnersOnInstall(skillDir, path, agent, sharedCtx);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (ctx.cavemanSeenPaths.has(path)) {
|
|
291
|
+
return withCodexSkillsFeature(agent, {
|
|
292
|
+
path,
|
|
293
|
+
action: "unchanged",
|
|
294
|
+
note: "shared skill path already written",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
ctx.cavemanSeenPaths.add(path);
|
|
298
|
+
|
|
299
|
+
const existed = await Bun.file(path).exists();
|
|
300
|
+
if (existed && (await Bun.file(path).text()) === CAVEMAN_SKILL_MD) {
|
|
301
|
+
return withCodexSkillsFeature(agent, { path, action: "unchanged" });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await mkdir(skillDir, { recursive: true });
|
|
305
|
+
await Bun.write(path, CAVEMAN_SKILL_MD);
|
|
306
|
+
return withCodexSkillsFeature(agent, {
|
|
307
|
+
path,
|
|
308
|
+
action: existed ? "updated" : "created",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function stePackMatches(skillDir: string): Promise<boolean> {
|
|
313
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
314
|
+
if (!(await Bun.file(skillPath).exists())) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if ((await Bun.file(skillPath).text()) !== STE_SKILL_MD) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
for (const file of STE_REFERENCE_FILES) {
|
|
321
|
+
const path = join(skillDir, file.relativePath);
|
|
322
|
+
if (!(await Bun.file(path).exists()) || (await Bun.file(path).text()) !== file.content) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function removeSteSkillFiles(
|
|
330
|
+
skillDir: string,
|
|
331
|
+
skillPath: string,
|
|
332
|
+
): Promise<Extract<InstallAction, "removed" | "not-found">> {
|
|
333
|
+
let removedReference = false;
|
|
334
|
+
for (const file of STE_REFERENCE_FILES) {
|
|
335
|
+
const referencePath = join(skillDir, file.relativePath);
|
|
336
|
+
if (await Bun.file(referencePath).exists()) {
|
|
337
|
+
await unlink(referencePath);
|
|
338
|
+
removedReference = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Match Caveman's ownership model: remove Miru's known files only. Leave
|
|
343
|
+
// any user-created files in this skill directory untouched.
|
|
344
|
+
try {
|
|
345
|
+
await rmdir(join(skillDir, "references"));
|
|
346
|
+
} catch {
|
|
347
|
+
// User files may keep the directory non-empty.
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const skillAction = await removeSkillMdAndOwners(skillPath, skillDir);
|
|
351
|
+
return removedReference && skillAction === "not-found" ? "removed" : skillAction;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function applySte(
|
|
355
|
+
agent: AgentTarget,
|
|
356
|
+
mode: InstallMode,
|
|
357
|
+
ctx: ApplyCtx = {
|
|
358
|
+
selectedAgents: [agent],
|
|
359
|
+
cavemanSeenPaths: new Set(),
|
|
360
|
+
steSeenPaths: new Set(),
|
|
361
|
+
},
|
|
362
|
+
): Promise<WriteResult | null> {
|
|
363
|
+
const skillDir = agent.steSkillDir;
|
|
364
|
+
if (!skillDir) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
369
|
+
const allAgents = ctx.allAgents ?? AGENT_TARGETS;
|
|
370
|
+
const shared = isSharedSkillPath(skillPath, allAgents, steSkillPathOf);
|
|
371
|
+
const sharedCtx = {
|
|
372
|
+
selectedAgents: ctx.selectedAgents,
|
|
373
|
+
allAgents: ctx.allAgents,
|
|
374
|
+
isDetected: ctx.isDetected,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (mode === "uninstall") {
|
|
378
|
+
if (shared) {
|
|
379
|
+
const decision = await resolveSharedSkillUninstall(
|
|
380
|
+
skillPath,
|
|
381
|
+
skillDir,
|
|
382
|
+
agent,
|
|
383
|
+
sharedCtx,
|
|
384
|
+
steSkillPathOf,
|
|
385
|
+
);
|
|
386
|
+
if (decision.kind === "defer" || decision.kind === "keep") {
|
|
387
|
+
return { path: skillPath, action: "unchanged", note: decision.note };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { path: skillPath, action: await removeSteSkillFiles(skillDir, skillPath) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (shared) {
|
|
394
|
+
await ensureSharedSkillOwnersOnInstall(skillDir, skillPath, agent, sharedCtx, steSkillPathOf);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (ctx.steSeenPaths.has(skillPath)) {
|
|
398
|
+
return withCodexSkillsFeature(agent, {
|
|
399
|
+
path: skillPath,
|
|
400
|
+
action: "unchanged",
|
|
401
|
+
note: "shared skill path already written",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
ctx.steSeenPaths.add(skillPath);
|
|
405
|
+
|
|
406
|
+
const existed = await Bun.file(skillPath).exists();
|
|
407
|
+
if (existed && (await stePackMatches(skillDir))) {
|
|
408
|
+
return withCodexSkillsFeature(agent, { path: skillPath, action: "unchanged" });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await mkdir(join(skillDir, "references"), { recursive: true });
|
|
412
|
+
await Bun.write(skillPath, STE_SKILL_MD);
|
|
413
|
+
for (const file of STE_REFERENCE_FILES) {
|
|
414
|
+
await Bun.write(join(skillDir, file.relativePath), file.content);
|
|
415
|
+
}
|
|
416
|
+
return withCodexSkillsFeature(agent, {
|
|
417
|
+
path: skillPath,
|
|
418
|
+
action: existed ? "updated" : "created",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
186
422
|
const INTEGRATIONS: Integration[] = [
|
|
187
423
|
{
|
|
188
424
|
id: "mcp",
|
|
@@ -221,26 +457,94 @@ const INTEGRATIONS: Integration[] = [
|
|
|
221
457
|
planPath: (agent) => agent.hooksPath,
|
|
222
458
|
apply: applyHooks,
|
|
223
459
|
},
|
|
460
|
+
{
|
|
461
|
+
id: "caveman",
|
|
462
|
+
label: "Caveman",
|
|
463
|
+
description: "on-demand chat compression skill (/caveman)",
|
|
464
|
+
experimental: true,
|
|
465
|
+
defaultChecked: false,
|
|
466
|
+
planPath: (agent) => agent.cavemanSkillPath,
|
|
467
|
+
apply: applyCaveman,
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: "ste",
|
|
471
|
+
label: "STE writing",
|
|
472
|
+
description: "on-demand clear technical English for docs (/ste)",
|
|
473
|
+
experimental: true,
|
|
474
|
+
defaultChecked: false,
|
|
475
|
+
planPath: (agent) => (agent.steSkillDir ? join(agent.steSkillDir, "SKILL.md") : null),
|
|
476
|
+
apply: applySte,
|
|
477
|
+
},
|
|
224
478
|
];
|
|
225
479
|
|
|
480
|
+
function integrationApplies(integration: Integration, agent: AgentTarget): boolean {
|
|
481
|
+
return integration.planPath(agent) !== null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function integrationsForAgents(agents: AgentTarget[]): Integration[] {
|
|
485
|
+
return INTEGRATIONS.filter((integration) =>
|
|
486
|
+
agents.some((agent) => integrationApplies(integration, agent)),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
226
490
|
function formatActionLine(integration: Integration, result: WriteResult): string {
|
|
227
491
|
const icon = ACTION_ICON[result.action] ?? dim("·");
|
|
228
|
-
const detail = ACTION_DETAIL[result.action];
|
|
492
|
+
const detail = result.note ?? ACTION_DETAIL[result.action];
|
|
229
493
|
const suffix = detail ? dim(` — ${detail}`) : "";
|
|
230
494
|
return ` ${icon} ${integration.label.padEnd(13)} ${dim(result.action)}${suffix}\n ${dim(result.path)}`;
|
|
231
495
|
}
|
|
232
496
|
|
|
233
|
-
|
|
497
|
+
/** Plan footnote for Codex + skills (Caveman / STE); null when nothing should be shown. */
|
|
498
|
+
export function codexSkillsPlanNote(
|
|
499
|
+
mode: InstallMode,
|
|
500
|
+
skillsFeatureEnabled: boolean,
|
|
501
|
+
): string | null {
|
|
502
|
+
if (mode === "install") {
|
|
503
|
+
return "also sets [features] skills = true in ~/.codex/config.toml";
|
|
504
|
+
}
|
|
505
|
+
if (skillsFeatureEnabled) {
|
|
506
|
+
return "leaves [features] skills = true in ~/.codex/config.toml";
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** @deprecated Prefer codexSkillsPlanNote. */
|
|
512
|
+
export const codexCavemanPlanNote = codexSkillsPlanNote;
|
|
513
|
+
|
|
514
|
+
async function printPlan(
|
|
515
|
+
mode: InstallMode,
|
|
516
|
+
agents: AgentTarget[],
|
|
517
|
+
integrations: Integration[],
|
|
518
|
+
): Promise<void> {
|
|
234
519
|
writeStdout("");
|
|
235
520
|
writeStdout(dim("Plan"));
|
|
236
521
|
divider();
|
|
237
522
|
|
|
238
523
|
for (const agent of agents) {
|
|
239
524
|
writeStdout(` ${agent.displayName}`);
|
|
525
|
+
let printedCodexSkillsNote = false;
|
|
240
526
|
for (const integration of integrations) {
|
|
241
527
|
const path = integration.planPath(agent);
|
|
242
|
-
|
|
243
|
-
|
|
528
|
+
if (!path) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
writeStdout(` ${green("✓")} ${integration.label.padEnd(13)} ${path}`);
|
|
532
|
+
if (
|
|
533
|
+
!printedCodexSkillsNote &&
|
|
534
|
+
(integration.id === "caveman" || integration.id === "ste") &&
|
|
535
|
+
agent.id === "codex"
|
|
536
|
+
) {
|
|
537
|
+
let skillsEnabled = false;
|
|
538
|
+
const configPath = agent.mcp?.path;
|
|
539
|
+
if (mode === "uninstall" && configPath && (await Bun.file(configPath).exists())) {
|
|
540
|
+
skillsEnabled = codexSkillsFeatureEnabled(await Bun.file(configPath).text());
|
|
541
|
+
}
|
|
542
|
+
const note = codexSkillsPlanNote(mode, skillsEnabled);
|
|
543
|
+
if (note) {
|
|
544
|
+
writeStdout(` ${dim(note)}`);
|
|
545
|
+
printedCodexSkillsNote = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
244
548
|
}
|
|
245
549
|
}
|
|
246
550
|
writeStdout("");
|
|
@@ -255,12 +559,20 @@ async function apply(
|
|
|
255
559
|
writeStdout(dim(mode === "install" ? "Installing" : "Removing"));
|
|
256
560
|
divider();
|
|
257
561
|
|
|
562
|
+
const ctx: ApplyCtx = {
|
|
563
|
+
selectedAgents: agents,
|
|
564
|
+
cavemanSeenPaths: new Set(),
|
|
565
|
+
steSeenPaths: new Set(),
|
|
566
|
+
};
|
|
567
|
+
|
|
258
568
|
for (const agent of agents) {
|
|
259
569
|
writeStdout(` ${agent.displayName}`);
|
|
260
570
|
for (const integration of integrations) {
|
|
261
|
-
|
|
571
|
+
if (!integrationApplies(integration, agent)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const result = await integration.apply(agent, mode, ctx);
|
|
262
575
|
if (!result) {
|
|
263
|
-
writeStdout(` ${dim("–")} ${integration.id.padEnd(13)} ${dim("not supported")}`);
|
|
264
576
|
continue;
|
|
265
577
|
}
|
|
266
578
|
writeStdout(formatActionLine(integration, result));
|
|
@@ -317,7 +629,13 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
317
629
|
return;
|
|
318
630
|
}
|
|
319
631
|
|
|
320
|
-
const
|
|
632
|
+
const availableIntegrations = integrationsForAgents(chosenAgents);
|
|
633
|
+
if (availableIntegrations.length === 0) {
|
|
634
|
+
hint("No integrations apply to the selected agents. Exiting.");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const integrationItems = availableIntegrations.map((integration) => ({
|
|
321
639
|
label: `${integration.label}${integration.experimental ? dim(" (experimental)") : ""} — ${integration.description}`,
|
|
322
640
|
value: integration,
|
|
323
641
|
checked: install ? (integration.defaultChecked ?? true) : true,
|
|
@@ -333,7 +651,7 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
333
651
|
return;
|
|
334
652
|
}
|
|
335
653
|
|
|
336
|
-
printPlan(chosenAgents, chosenIntegrations);
|
|
654
|
+
await printPlan(mode, chosenAgents, chosenIntegrations);
|
|
337
655
|
|
|
338
656
|
const proceed = await promptConfirm(install ? "Proceed?" : "Remove miru configuration?", install);
|
|
339
657
|
if (!proceed) {
|
|
@@ -355,11 +673,14 @@ export async function runInstaller(mode: InstallMode): Promise<void> {
|
|
|
355
673
|
}
|
|
356
674
|
|
|
357
675
|
export {
|
|
676
|
+
applyCaveman,
|
|
358
677
|
applyHooks,
|
|
359
678
|
applyInstructions,
|
|
360
679
|
applyMcp,
|
|
680
|
+
applySte,
|
|
361
681
|
applySubagent,
|
|
362
682
|
INTEGRATIONS,
|
|
683
|
+
integrationsForAgents,
|
|
363
684
|
mergeJsonMember as mergeMcpJson,
|
|
364
685
|
removeJsonMember as removeMcpJson,
|
|
365
686
|
};
|
package/src/installer/prompt.ts
CHANGED
|
@@ -163,6 +163,17 @@ export function parseInstallerKeyForTest(chunk: Buffer): string {
|
|
|
163
163
|
return parseKey(chunk);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** Map confirm keys to the next Yes selection. Left/← and y → Yes; right/→ and n → No. */
|
|
167
|
+
export function nextConfirmYesSelected(yesSelected: boolean, key: string): boolean {
|
|
168
|
+
if (key === "left" || key === "yes") {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (key === "right" || key === "no") {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return yesSelected;
|
|
175
|
+
}
|
|
176
|
+
|
|
166
177
|
async function withRawMode<T>(fn: () => Promise<T>): Promise<T> {
|
|
167
178
|
input.setRawMode(true);
|
|
168
179
|
input.resume();
|
|
@@ -328,14 +339,10 @@ export async function promptConfirm(question: string, defaultYes = true): Promis
|
|
|
328
339
|
writeRaw("\n");
|
|
329
340
|
return yesSelected;
|
|
330
341
|
}
|
|
331
|
-
if (key === "left" || key === "no") {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
} else if (key === "right" || key === "yes") {
|
|
337
|
-
if (!yesSelected) {
|
|
338
|
-
yesSelected = true;
|
|
342
|
+
if (key === "left" || key === "right" || key === "yes" || key === "no") {
|
|
343
|
+
const next = nextConfirmYesSelected(yesSelected, key);
|
|
344
|
+
if (next !== yesSelected) {
|
|
345
|
+
yesSelected = next;
|
|
339
346
|
updateConfirmPromptLine(question, yesSelected);
|
|
340
347
|
}
|
|
341
348
|
}
|
|
@@ -88,7 +88,7 @@ export const INSTRUCTIONS_MARKDOWN = `## Miru Code Search
|
|
|
88
88
|
|
|
89
89
|
${SEARCH_POLICY_BODY}
|
|
90
90
|
|
|
91
|
-
Run \`miru setup\` once — the MCP server loads
|
|
91
|
+
Run \`miru setup\` once — the MCP server loads credentials from \`credentials.json\`, and interactive first use can auto-start device login when nothing is stored.
|
|
92
92
|
|
|
93
93
|
CLI fallback when MCP is unavailable:
|
|
94
94
|
|
|
@@ -123,7 +123,8 @@ export const MCP_SERVER_INSTRUCTIONS =
|
|
|
123
123
|
"(3) `find_related` traces similar code in other files, not more context in the same file. " +
|
|
124
124
|
"(4) `locate` finds exact substrings (env vars, symbols, error codes) — prefer over Grep; use mode=count or locations when possible. " +
|
|
125
125
|
"Always pass the project root as `repo`. Local repos return `absolute_path` on each hit — use Read only to edit. " +
|
|
126
|
-
"Native Grep/Glob only outside the indexed repo or for non-code tasks."
|
|
126
|
+
"Native Grep/Glob only outside the indexed repo or for non-code tasks. " +
|
|
127
|
+
"On credential errors, call this server's `auth` tool — not the host `mcp_auth` tool.";
|
|
127
128
|
|
|
128
129
|
export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
|
|
129
130
|
`${MCP_SERVER_INSTRUCTIONS} ` +
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { mkdir, rmdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AGENT_TARGETS, type AgentTarget, type InstallAction, isAgentDetected } from "./agents.ts";
|
|
4
|
+
|
|
5
|
+
/** Sidecar next to a shared Agent Skill (`SKILL.md`) tracking which IDEs own it. */
|
|
6
|
+
export const SKILL_OWNERS_FILE = "miru-owners.json";
|
|
7
|
+
|
|
8
|
+
/** Context for shared-path skill install/uninstall (Caveman, STE, …). */
|
|
9
|
+
export interface SharedSkillCtx {
|
|
10
|
+
selectedAgents: AgentTarget[];
|
|
11
|
+
allAgents?: AgentTarget[];
|
|
12
|
+
isDetected?: (agent: AgentTarget) => Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SkillPathOf = (agent: AgentTarget) => string | null;
|
|
16
|
+
|
|
17
|
+
function uniqueIds(ids: Iterable<string>): string[] {
|
|
18
|
+
return Array.from(new Set(ids));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolvedAgents(ctx: SharedSkillCtx): AgentTarget[] {
|
|
22
|
+
return ctx.allAgents ?? AGENT_TARGETS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function agentsSharingPath(
|
|
26
|
+
path: string,
|
|
27
|
+
allAgents: AgentTarget[],
|
|
28
|
+
pathOf: SkillPathOf,
|
|
29
|
+
): AgentTarget[] {
|
|
30
|
+
return allAgents.filter((agent) => pathOf(agent) === path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isSharedSkillPath(
|
|
34
|
+
path: string,
|
|
35
|
+
allAgents: AgentTarget[],
|
|
36
|
+
pathOf: SkillPathOf,
|
|
37
|
+
): boolean {
|
|
38
|
+
return agentsSharingPath(path, allAgents, pathOf).length > 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readSkillOwners(skillDir: string): Promise<string[] | null> {
|
|
42
|
+
const ownersPath = join(skillDir, SKILL_OWNERS_FILE);
|
|
43
|
+
if (!(await Bun.file(ownersPath).exists())) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(await Bun.file(ownersPath).text()) as unknown;
|
|
48
|
+
if (!Array.isArray(parsed)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function writeSkillOwners(skillDir: string, owners: string[]): Promise<void> {
|
|
58
|
+
await mkdir(skillDir, { recursive: true });
|
|
59
|
+
await Bun.write(
|
|
60
|
+
join(skillDir, SKILL_OWNERS_FILE),
|
|
61
|
+
`${JSON.stringify([...owners].sort(), null, 2)}\n`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stamp shared-path ownership on install.
|
|
67
|
+
* - Existing owners file: add current agent.
|
|
68
|
+
* - Fresh skill: start with current agent only.
|
|
69
|
+
* - Legacy skill (file exists, no owners): seed from detected siblings + current;
|
|
70
|
+
* skip writing a singleton so uninstall stays on the legacy keep path.
|
|
71
|
+
*/
|
|
72
|
+
export async function ensureSharedSkillOwnersOnInstall(
|
|
73
|
+
skillDir: string,
|
|
74
|
+
skillPath: string,
|
|
75
|
+
agent: AgentTarget,
|
|
76
|
+
ctx: SharedSkillCtx,
|
|
77
|
+
pathOf: SkillPathOf,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const existing = await readSkillOwners(skillDir);
|
|
80
|
+
if (existing !== null) {
|
|
81
|
+
await writeSkillOwners(skillDir, uniqueIds([...existing, agent.id]));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!(await Bun.file(skillPath).exists())) {
|
|
86
|
+
await writeSkillOwners(skillDir, [agent.id]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const detect = ctx.isDetected ?? isAgentDetected;
|
|
91
|
+
const seeded: string[] = [];
|
|
92
|
+
for (const member of agentsSharingPath(skillPath, resolvedAgents(ctx), pathOf)) {
|
|
93
|
+
if (member.id === agent.id || (await detect(member))) {
|
|
94
|
+
seeded.push(member.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const unique = uniqueIds(seeded);
|
|
98
|
+
if (unique.length <= 1) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await writeSkillOwners(skillDir, unique);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function shouldKeepSharedSkill(
|
|
105
|
+
path: string,
|
|
106
|
+
agent: AgentTarget,
|
|
107
|
+
ctx: SharedSkillCtx,
|
|
108
|
+
pathOf: SkillPathOf,
|
|
109
|
+
): Promise<boolean> {
|
|
110
|
+
const detect = ctx.isDetected ?? isAgentDetected;
|
|
111
|
+
const siblings = agentsSharingPath(path, resolvedAgents(ctx), pathOf).filter(
|
|
112
|
+
(other) => other.id !== agent.id,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
for (const sibling of siblings) {
|
|
116
|
+
if (ctx.selectedAgents.some((selected) => selected.id === sibling.id)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (await detect(sibling)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type SharedSkillUninstallDecision =
|
|
127
|
+
| { kind: "defer"; note: string }
|
|
128
|
+
| { kind: "keep"; note: string }
|
|
129
|
+
| { kind: "remove" };
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decide whether a shared-path skill uninstall should defer, keep, or remove.
|
|
133
|
+
* Caller removes files only on `remove`.
|
|
134
|
+
*/
|
|
135
|
+
export async function resolveSharedSkillUninstall(
|
|
136
|
+
path: string,
|
|
137
|
+
skillDir: string,
|
|
138
|
+
agent: AgentTarget,
|
|
139
|
+
ctx: SharedSkillCtx,
|
|
140
|
+
pathOf: SkillPathOf,
|
|
141
|
+
): Promise<SharedSkillUninstallDecision> {
|
|
142
|
+
const selectedSharing = agentsSharingPath(path, ctx.selectedAgents, pathOf);
|
|
143
|
+
const isLast =
|
|
144
|
+
selectedSharing.length === 0 || selectedSharing[selectedSharing.length - 1]?.id === agent.id;
|
|
145
|
+
if (!isLast) {
|
|
146
|
+
return {
|
|
147
|
+
kind: "defer",
|
|
148
|
+
note: "shared skill; remove deferred to last selected IDE",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const owners = await readSkillOwners(skillDir);
|
|
153
|
+
if (owners !== null) {
|
|
154
|
+
const removing = new Set<string>(selectedSharing.map((selected) => selected.id));
|
|
155
|
+
const remaining = owners.filter((ownerId) => !removing.has(ownerId));
|
|
156
|
+
if (remaining.length > 0) {
|
|
157
|
+
await writeSkillOwners(skillDir, remaining);
|
|
158
|
+
return {
|
|
159
|
+
kind: "keep",
|
|
160
|
+
note: "shared skill kept — still owned by another IDE",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { kind: "remove" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (await shouldKeepSharedSkill(path, agent, ctx, pathOf)) {
|
|
167
|
+
return {
|
|
168
|
+
kind: "keep",
|
|
169
|
+
note: "shared skill kept — another IDE still uses this path",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { kind: "remove" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Remove SKILL.md and owners sidecar; leave sibling skill folders untouched. */
|
|
176
|
+
export async function removeSkillMdAndOwners(
|
|
177
|
+
path: string,
|
|
178
|
+
skillDir: string,
|
|
179
|
+
): Promise<Extract<InstallAction, "removed" | "not-found">> {
|
|
180
|
+
let removedSomething = false;
|
|
181
|
+
|
|
182
|
+
if (await Bun.file(path).exists()) {
|
|
183
|
+
await unlink(path);
|
|
184
|
+
removedSomething = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ownersPath = join(skillDir, SKILL_OWNERS_FILE);
|
|
188
|
+
if (await Bun.file(ownersPath).exists()) {
|
|
189
|
+
await unlink(ownersPath);
|
|
190
|
+
removedSomething = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await rmdir(skillDir);
|
|
195
|
+
} catch {
|
|
196
|
+
// Directory not empty or already gone — leave other skills alone.
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return removedSomething ? "removed" : "not-found";
|
|
200
|
+
}
|