@hizliemre/horse-code 0.3.0 → 0.3.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/dist/{app-5FXHE7GX.js → app-4WN37LZ3.js} +65 -37
- package/dist/{chunk-UEWVVN5L.js → chunk-27F44PBD.js} +196 -1126
- package/dist/{chunk-XYZVZPAY.js → chunk-2WXG35EM.js} +19 -15
- package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
- package/dist/{chunk-YPZP7LYL.js → chunk-3ACDNDCG.js} +1 -1
- package/dist/{chunk-XEGQT5EN.js → chunk-4M6LXNG2.js} +1 -1
- package/dist/{chunk-6OSEQOYY.js → chunk-6S4WWQMN.js} +2 -2
- package/dist/{chunk-LLL7QWXB.js → chunk-BFIZMM4G.js} +6 -6
- package/dist/chunk-CYLPQWIF.js +214 -0
- package/dist/{chunk-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
- package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
- package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
- package/dist/chunk-YULQ4URQ.js +1220 -0
- package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
- package/dist/cli.js +254 -66
- package/dist/{fix-ONLA45HD.js → fix-QCL5AITT.js} +9 -8
- package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
- package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
- package/dist/run-V5ZLZ3LS.js +274 -0
- package/dist/{save-skills-ZW5GY6KV.js → save-skills-NPKTYNAF.js} +2 -2
- package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
- package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
- package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
- package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
- package/dist/{verify-LC57A6H2.js → verify-HWZBTK5X.js} +15 -12
- package/package.json +1 -1
- package/dist/chunk-MRZVA5JB.js +0 -163
- package/dist/{chunk-EAF22QIG.js → chunk-JR2JLRE3.js} +3 -3
|
@@ -1,155 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadMigrated,
|
|
3
|
-
recordMigrated
|
|
4
|
-
} from "./chunk-63E73TGI.js";
|
|
5
|
-
import "./chunk-6W4UH2BQ.js";
|
|
6
|
-
|
|
7
|
-
// src/migrate/run.ts
|
|
8
|
-
import { cp, lstat, mkdir, readFile as readFile2, readlink, rm } from "fs/promises";
|
|
9
|
-
import { dirname, join as join2, resolve } from "path";
|
|
10
|
-
|
|
11
|
-
// src/migrate/discover.ts
|
|
12
|
-
import { readFile, readdir, stat } from "fs/promises";
|
|
13
|
-
import { join } from "path";
|
|
14
|
-
var RULE_FILES = [
|
|
15
|
-
{ file: "CLAUDE.md", tool: "Claude Code" },
|
|
16
|
-
{ file: "CLAUDE.local.md", tool: "Claude Code" },
|
|
17
|
-
{ file: "AGENTS.md", tool: "Codex / OpenAI" },
|
|
18
|
-
{ file: "GEMINI.md", tool: "Gemini CLI" },
|
|
19
|
-
{ file: ".cursorrules", tool: "Cursor" },
|
|
20
|
-
{ file: ".windsurfrules", tool: "Windsurf" },
|
|
21
|
-
{ file: ".clinerules", tool: "Cline" },
|
|
22
|
-
{ file: ".continuerules", tool: "Continue" },
|
|
23
|
-
{ file: ".aider.conf.yml", tool: "Aider" },
|
|
24
|
-
{ file: "CONVENTIONS.md", tool: "Aider" },
|
|
25
|
-
{ file: ".rules", tool: "Zed" },
|
|
26
|
-
{ file: ".github/copilot-instructions.md", tool: "GitHub Copilot" },
|
|
27
|
-
/**
|
|
28
|
-
* Not a tool's files — the project's own product and design context, which several skills read as their
|
|
29
|
-
* source of truth. They come as a PAIR: the skill that consumes them prints "the project's PRODUCT.md (and
|
|
30
|
-
* DESIGN.md when present)", so taking one without the other imports half a brief.
|
|
31
|
-
*
|
|
32
|
-
* They are here because they are instruction material by any test that matters: a real project's DESIGN.md
|
|
33
|
-
* held 17 KB of binding conventions (tokens, type scale, spacing, what never to do) and its PRODUCT.md the
|
|
34
|
-
* register those conventions serve. Leaving them behind meant every agent rebuilt its taste from scratch
|
|
35
|
-
* while the answer sat in the repo root.
|
|
36
|
-
*/
|
|
37
|
-
{ file: "PRODUCT.md", tool: "project convention", own: true },
|
|
38
|
-
{ file: "DESIGN.md", tool: "project convention", own: true }
|
|
39
|
-
];
|
|
40
|
-
var RULE_DIRS = [
|
|
41
|
-
{ dir: ".cursor/rules", tool: "Cursor", ext: /\.mdc?$/ },
|
|
42
|
-
{ dir: ".github/instructions", tool: "GitHub Copilot", ext: /\.md$/ }
|
|
43
|
-
];
|
|
44
|
-
var MAX_FINDING_BYTES = 6e4;
|
|
45
|
-
function claudeProjectSlug(cwd) {
|
|
46
|
-
return cwd.replace(/\//g, "-");
|
|
47
|
-
}
|
|
48
|
-
async function read(path) {
|
|
49
|
-
try {
|
|
50
|
-
const s = await stat(path);
|
|
51
|
-
if (!s.isFile() || s.size === 0) return void 0;
|
|
52
|
-
if (s.size > MAX_FINDING_BYTES) return { bytes: s.size };
|
|
53
|
-
return { bytes: s.size, text: await readFile(path, "utf8") };
|
|
54
|
-
} catch {
|
|
55
|
-
return void 0;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function listDir(dir) {
|
|
59
|
-
try {
|
|
60
|
-
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name);
|
|
61
|
-
} catch {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async function discover(opts) {
|
|
66
|
-
const { cwd, home } = opts;
|
|
67
|
-
const out = [];
|
|
68
|
-
const push = async (kind, tool, path, label, own = false) => {
|
|
69
|
-
const r = await read(path);
|
|
70
|
-
if (r) out.push({ kind, tool, path, label, bytes: r.bytes, ...own ? { own } : {}, ...r.text ? { text: r.text } : {} });
|
|
71
|
-
};
|
|
72
|
-
for (const { file, tool, own } of RULE_FILES) await push("rules", tool, join(cwd, file), file, own);
|
|
73
|
-
await push("rules", "Claude Code (user-level)", join(home, ".claude", "CLAUDE.md"), "~/.claude/CLAUDE.md");
|
|
74
|
-
await push("rules", "Codex (user-level)", join(home, ".codex", "AGENTS.md"), "~/.codex/AGENTS.md");
|
|
75
|
-
for (const { dir, tool, ext } of RULE_DIRS) {
|
|
76
|
-
for (const name of await listDir(join(cwd, dir))) {
|
|
77
|
-
if (ext.test(name)) await push("rules", tool, join(cwd, dir, name), `${dir}/${name}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const memDir = join(home, ".claude", "projects", claudeProjectSlug(cwd), "memory");
|
|
81
|
-
for (const name of await listDir(memDir)) {
|
|
82
|
-
if (!name.endsWith(".md") || name === "MEMORY.md") continue;
|
|
83
|
-
await push("memory", "Claude Code", join(memDir, name), `memory/${name}`);
|
|
84
|
-
}
|
|
85
|
-
const seenSkills = /* @__PURE__ */ new Set();
|
|
86
|
-
for (const [rel, tool] of [[".claude/skills", "Claude Code"], [".agents/skills", "agent skills"]]) {
|
|
87
|
-
const skillsDir = join(cwd, rel);
|
|
88
|
-
let entries;
|
|
89
|
-
try {
|
|
90
|
-
entries = await readdir(skillsDir, { withFileTypes: true });
|
|
91
|
-
} catch {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
for (const e of entries) {
|
|
95
|
-
if (seenSkills.has(e.name)) continue;
|
|
96
|
-
if (!e.isDirectory()) {
|
|
97
|
-
if (!e.isSymbolicLink()) continue;
|
|
98
|
-
try {
|
|
99
|
-
if (!(await stat(join(skillsDir, e.name))).isDirectory()) continue;
|
|
100
|
-
} catch {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const before = out.length;
|
|
105
|
-
await push("skill", tool, join(skillsDir, e.name, "SKILL.md"), `${rel}/${e.name}`);
|
|
106
|
-
if (out.length > before) seenSkills.add(e.name);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
for (const [file, tool] of [[".mcp.json", "Claude Code"], [".cursor/mcp.json", "Cursor"], [".vscode/mcp.json", "VS Code"]]) {
|
|
110
|
-
await push("mcp", tool, join(cwd, file), file);
|
|
111
|
-
}
|
|
112
|
-
for (const settings of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
113
|
-
await push("mcp", "Claude Code", join(cwd, settings), settings);
|
|
114
|
-
}
|
|
115
|
-
for (const [dir, kind] of [[".claude/agents", "agent"], [".claude/commands", "command"]]) {
|
|
116
|
-
for (const name of await listDir(join(cwd, dir))) {
|
|
117
|
-
if (name.endsWith(".md")) await push(kind, "Claude Code", join(cwd, dir, name), `${dir}/${name}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return out;
|
|
121
|
-
}
|
|
122
|
-
function summarize(findings) {
|
|
123
|
-
if (!findings.length) return "No configuration from another coding tool was found in this project.";
|
|
124
|
-
const byKind = /* @__PURE__ */ new Map();
|
|
125
|
-
for (const f of findings) {
|
|
126
|
-
const list = byKind.get(f.kind);
|
|
127
|
-
if (list) list.push(f);
|
|
128
|
-
else byKind.set(f.kind, [f]);
|
|
129
|
-
}
|
|
130
|
-
const LABEL = {
|
|
131
|
-
rules: "Instruction files",
|
|
132
|
-
memory: "Remembered facts",
|
|
133
|
-
skill: "Skills",
|
|
134
|
-
mcp: "MCP / settings",
|
|
135
|
-
agent: "Subagents",
|
|
136
|
-
command: "Slash commands"
|
|
137
|
-
};
|
|
138
|
-
const rows = [];
|
|
139
|
-
for (const kind of ["rules", "memory", "skill", "mcp", "agent", "command"]) {
|
|
140
|
-
const list = byKind.get(kind);
|
|
141
|
-
if (!list?.length) continue;
|
|
142
|
-
const tools = [...new Set(list.map((f) => f.tool))].join(", ");
|
|
143
|
-
const files = list.slice(0, 4).map((f) => `\`${f.label}\``).join(", ");
|
|
144
|
-
const more = list.length > 4 ? ` +${list.length - 4} more` : "";
|
|
145
|
-
rows.push(`- **${LABEL[kind]}** (${list.length}, from ${tools}): ${files}${more}`);
|
|
146
|
-
}
|
|
147
|
-
return rows.join("\n");
|
|
148
|
-
}
|
|
149
|
-
function hasAnything(findings) {
|
|
150
|
-
return findings.some((f) => f.kind === "rules" || f.kind === "memory" || f.kind === "skill" || f.kind === "mcp");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
1
|
// src/migrate/extract.ts
|
|
154
2
|
var BATCH = 20;
|
|
155
3
|
var SYSTEM = "You migrate instructions from one coding assistant to another. You are precise about what does and does not transfer, and you never invent an instruction that was not there.";
|
|
@@ -350,260 +198,156 @@ async function consolidateRules(opts) {
|
|
|
350
198
|
}
|
|
351
199
|
}
|
|
352
200
|
|
|
353
|
-
// src/migrate/
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
201
|
+
// src/migrate/discover.ts
|
|
202
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
203
|
+
import { join } from "path";
|
|
204
|
+
var RULE_FILES = [
|
|
205
|
+
{ file: "CLAUDE.md", tool: "Claude Code" },
|
|
206
|
+
{ file: "CLAUDE.local.md", tool: "Claude Code" },
|
|
207
|
+
{ file: "AGENTS.md", tool: "Codex / OpenAI" },
|
|
208
|
+
{ file: "GEMINI.md", tool: "Gemini CLI" },
|
|
209
|
+
{ file: ".cursorrules", tool: "Cursor" },
|
|
210
|
+
{ file: ".windsurfrules", tool: "Windsurf" },
|
|
211
|
+
{ file: ".clinerules", tool: "Cline" },
|
|
212
|
+
{ file: ".continuerules", tool: "Continue" },
|
|
213
|
+
{ file: ".aider.conf.yml", tool: "Aider" },
|
|
214
|
+
{ file: "CONVENTIONS.md", tool: "Aider" },
|
|
215
|
+
{ file: ".rules", tool: "Zed" },
|
|
216
|
+
{ file: ".github/copilot-instructions.md", tool: "GitHub Copilot" },
|
|
217
|
+
/**
|
|
218
|
+
* Not a tool's files — the project's own product and design context, which several skills read as their
|
|
219
|
+
* source of truth. They come as a PAIR: the skill that consumes them prints "the project's PRODUCT.md (and
|
|
220
|
+
* DESIGN.md when present)", so taking one without the other imports half a brief.
|
|
221
|
+
*
|
|
222
|
+
* They are here because they are instruction material by any test that matters: a real project's DESIGN.md
|
|
223
|
+
* held 17 KB of binding conventions (tokens, type scale, spacing, what never to do) and its PRODUCT.md the
|
|
224
|
+
* register those conventions serve. Leaving them behind meant every agent rebuilt its taste from scratch
|
|
225
|
+
* while the answer sat in the repo root.
|
|
226
|
+
*/
|
|
227
|
+
{ file: "PRODUCT.md", tool: "project convention", own: true },
|
|
228
|
+
{ file: "DESIGN.md", tool: "project convention", own: true }
|
|
229
|
+
];
|
|
230
|
+
var RULE_DIRS = [
|
|
231
|
+
{ dir: ".cursor/rules", tool: "Cursor", ext: /\.mdc?$/ },
|
|
232
|
+
{ dir: ".github/instructions", tool: "GitHub Copilot", ext: /\.md$/ }
|
|
233
|
+
];
|
|
234
|
+
var MAX_FINDING_BYTES = 6e4;
|
|
235
|
+
function claudeProjectSlug(cwd) {
|
|
236
|
+
return cwd.replace(/\//g, "-");
|
|
357
237
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
238
|
+
async function readFinding(path) {
|
|
239
|
+
try {
|
|
240
|
+
const s = await stat(path);
|
|
241
|
+
if (!s.isFile() || s.size === 0) return void 0;
|
|
242
|
+
if (s.size > MAX_FINDING_BYTES) return { bytes: s.size };
|
|
243
|
+
return { bytes: s.size, text: await readFile(path, "utf8") };
|
|
244
|
+
} catch {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
362
247
|
}
|
|
363
|
-
async function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return result;
|
|
248
|
+
async function listDir(dir) {
|
|
249
|
+
try {
|
|
250
|
+
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name);
|
|
251
|
+
} catch {
|
|
252
|
+
return [];
|
|
369
253
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
385
|
-
});
|
|
386
|
-
deps.idle?.();
|
|
387
|
-
result.failedBatches = extraction.failed.length;
|
|
388
|
-
if (extraction.failed.length) {
|
|
389
|
-
const reasons = [...new Set(extraction.failed.map((f) => f.error))].slice(0, 3);
|
|
390
|
-
deps.note(`\u26A0\uFE0F **${extraction.failed.length} batch(es) could not be read** \u2014 ` + reasons.map((r) => `\`${r.slice(0, 160)}\``).join("; ") + `
|
|
391
|
-
|
|
392
|
-
_Nothing from those files was imported. Re-run \`/migrate\` once the cause above is cleared._`);
|
|
393
|
-
}
|
|
394
|
-
const raw = groupForReview(extraction);
|
|
395
|
-
result.skipped = raw.skipped.length;
|
|
396
|
-
let rules = raw.rules;
|
|
397
|
-
const facts = [...raw.facts];
|
|
398
|
-
if (rules.length > MAX_RULES) {
|
|
399
|
-
deps.note(`${rules.length} rule candidates \u2014 consolidating, since every rule is inlined into every prompt forever.`);
|
|
400
|
-
deps.busy?.("consolidating rules", deps.models[0]);
|
|
401
|
-
const c = await consolidateRules({
|
|
402
|
-
provider: deps.provider,
|
|
403
|
-
models: deps.models,
|
|
404
|
-
candidates: rules,
|
|
405
|
-
...deps.signal ? { signal: deps.signal } : {}
|
|
406
|
-
});
|
|
407
|
-
deps.idle?.();
|
|
408
|
-
if (c.rules.length < rules.length) {
|
|
409
|
-
deps.note(`Consolidated **${rules.length} \u2192 ${c.rules.length}** rules${c.demoted.length ? `, kept ${c.demoted.length} as fact(s)` : ""}${c.dropped.length ? `, dropped ${c.dropped.length} describing a workflow horse-code replaces` : ""}.`);
|
|
410
|
-
rules = c.rules;
|
|
411
|
-
facts.push(...c.demoted);
|
|
412
|
-
result.skipped += c.dropped.length;
|
|
413
|
-
} else {
|
|
414
|
-
deps.note(`\u26A0\uFE0F Consolidation did not run \u2014 you are being asked about all ${rules.length} candidates.`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (rules.length) {
|
|
418
|
-
const answer = await deps.ask(
|
|
419
|
-
`**${rules.length} standing rule(s)**. A rule goes into EVERY agent's instructions, for every task, permanently.
|
|
420
|
-
|
|
421
|
-
${sample(rules)}
|
|
422
|
-
|
|
423
|
-
Import them?`,
|
|
424
|
-
{ options: [
|
|
425
|
-
{ label: "Yes \u2014 import all", description: `${rules.length} rules become permanent memory` },
|
|
426
|
-
{ label: "No", description: "Nothing is written; you can re-run /migrate later" }
|
|
427
|
-
] }
|
|
428
|
-
);
|
|
429
|
-
if (YES.test(answer.trim())) {
|
|
430
|
-
const before = countKind(deps.memStore, "rule");
|
|
431
|
-
for (const c of rules) await deps.memStore.add(c.text, "rule");
|
|
432
|
-
result.rules = countKind(deps.memStore, "rule") - before;
|
|
433
|
-
} else result.declined.push("rules");
|
|
434
|
-
}
|
|
435
|
-
if (facts.length) {
|
|
436
|
-
const answer = await deps.ask(
|
|
437
|
-
`**${facts.length} project fact(s)** were found \u2014 recalled when relevant rather than always.
|
|
438
|
-
|
|
439
|
-
${sample(facts)}
|
|
440
|
-
|
|
441
|
-
Import them?`,
|
|
442
|
-
{ options: [
|
|
443
|
-
{ label: "Yes \u2014 import all", description: `${facts.length} facts become memory` },
|
|
444
|
-
{ label: "No", description: "Nothing is written" }
|
|
445
|
-
] }
|
|
446
|
-
);
|
|
447
|
-
if (YES.test(answer.trim())) {
|
|
448
|
-
const before = countKind(deps.memStore, "fact");
|
|
449
|
-
for (const c of facts) await deps.memStore.add(c.text, "fact");
|
|
450
|
-
result.facts = countKind(deps.memStore, "fact") - before;
|
|
451
|
-
} else result.declined.push("facts");
|
|
452
|
-
}
|
|
453
|
-
if (raw.skipped.length) {
|
|
454
|
-
const skipped = raw.skipped;
|
|
455
|
-
const why = skipped.slice(0, SAMPLE).map((c) => `- ~~${c.text.slice(0, 90)}~~ \u2014 ${c.reason}`).join("\n");
|
|
456
|
-
deps.note(`**Left behind (${skipped.length})** \u2014 these only meant something in the original tool:
|
|
457
|
-
${why}` + (skipped.length > SAMPLE ? `
|
|
458
|
-
- _\u2026and ${skipped.length - SAMPLE} more_` : "") + `
|
|
459
|
-
|
|
460
|
-
_If one of these should have transferred, add it with \`/memory add\`._`);
|
|
254
|
+
}
|
|
255
|
+
async function discover(opts) {
|
|
256
|
+
const { cwd, home } = opts;
|
|
257
|
+
const out = [];
|
|
258
|
+
const push = async (kind, tool, path, label, own = false) => {
|
|
259
|
+
const r = await readFinding(path);
|
|
260
|
+
if (r) out.push({ kind, tool, path, label, bytes: r.bytes, ...own ? { own } : {}, ...r.text ? { text: r.text } : {} });
|
|
261
|
+
};
|
|
262
|
+
for (const { file, tool, own } of RULE_FILES) await push("rules", tool, join(cwd, file), file, own);
|
|
263
|
+
await push("rules", "Claude Code (user-level)", join(home, ".claude", "CLAUDE.md"), "~/.claude/CLAUDE.md");
|
|
264
|
+
await push("rules", "Codex (user-level)", join(home, ".codex", "AGENTS.md"), "~/.codex/AGENTS.md");
|
|
265
|
+
for (const { dir, tool, ext } of RULE_DIRS) {
|
|
266
|
+
for (const name of await listDir(join(cwd, dir))) {
|
|
267
|
+
if (ext.test(name)) await push("rules", tool, join(cwd, dir, name), `${dir}/${name}`);
|
|
461
268
|
}
|
|
462
269
|
}
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const copied = [];
|
|
482
|
-
for (const [i, s] of skills.entries()) {
|
|
483
|
-
const name = s.label.split("/").pop();
|
|
484
|
-
deps.progress?.("copy", `\u{1F4E6} Copying **${i + 1}/${skills.length}** \u2014 \`${name}\``);
|
|
485
|
-
const dest = join2(deps.cwd, ".horsecode", "skills", name);
|
|
270
|
+
const memDir = join(home, ".claude", "projects", claudeProjectSlug(cwd), "memory");
|
|
271
|
+
for (const name of await listDir(memDir)) {
|
|
272
|
+
if (!name.endsWith(".md") || name === "MEMORY.md") continue;
|
|
273
|
+
await push("memory", "Claude Code", join(memDir, name), `memory/${name}`);
|
|
274
|
+
}
|
|
275
|
+
const seenSkills = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const [rel, tool] of [[".claude/skills", "Claude Code"], [".agents/skills", "agent skills"]]) {
|
|
277
|
+
const skillsDir = join(cwd, rel);
|
|
278
|
+
let entries;
|
|
279
|
+
try {
|
|
280
|
+
entries = await readdir(skillsDir, { withFileTypes: true });
|
|
281
|
+
} catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (const e of entries) {
|
|
285
|
+
if (seenSkills.has(e.name)) continue;
|
|
286
|
+
if (!e.isDirectory()) {
|
|
287
|
+
if (!e.isSymbolicLink()) continue;
|
|
486
288
|
try {
|
|
487
|
-
await
|
|
488
|
-
await cp(dirname(s.path), dest, { recursive: true, dereference: true });
|
|
489
|
-
copied.push(name);
|
|
490
|
-
result.skills++;
|
|
289
|
+
if (!(await stat(join(skillsDir, e.name))).isDirectory()) continue;
|
|
491
290
|
} catch {
|
|
291
|
+
continue;
|
|
492
292
|
}
|
|
493
293
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (!copied.includes(name)) continue;
|
|
499
|
-
const dir = dirname(s.path);
|
|
500
|
-
const paths = [dir];
|
|
501
|
-
try {
|
|
502
|
-
if ((await lstat(dir)).isSymbolicLink()) paths.push(resolve(dirname(dir), await readlink(dir)));
|
|
503
|
-
} catch {
|
|
504
|
-
}
|
|
505
|
-
removable.push({ name, paths });
|
|
506
|
-
}
|
|
507
|
-
const answer2 = await deps.ask(
|
|
508
|
-
`**${removable.length} skill(s)** are now in \`.horsecode/skills/\` AND still at their old path.
|
|
509
|
-
|
|
510
|
-
Remove the originals? horse-code reads its own directory, so the old copies are twins that will drift \u2014 but OTHER tools in this project read them too.
|
|
511
|
-
|
|
512
|
-
_Removed from: ${[...new Set(removable.flatMap((r) => r.paths.map((p) => dirname(p).replace(deps.cwd, "."))))].join(", ")}_`,
|
|
513
|
-
{ options: [
|
|
514
|
-
{ label: "No \u2014 keep them", description: "Nothing is deleted; the two copies can diverge" },
|
|
515
|
-
{ label: "Yes \u2014 remove the originals", description: `${removable.length} directory(ies) deleted from their old location` }
|
|
516
|
-
] }
|
|
517
|
-
);
|
|
518
|
-
if (YES.test(answer2.trim())) {
|
|
519
|
-
for (const [i, r] of removable.entries()) {
|
|
520
|
-
deps.progress?.("remove", `\u{1F9F9} Removing **${i + 1}/${removable.length}** \u2014 \`${r.name}\``);
|
|
521
|
-
for (const path of r.paths) {
|
|
522
|
-
try {
|
|
523
|
-
await rm(path, { recursive: true, force: true });
|
|
524
|
-
result.removed++;
|
|
525
|
-
} catch {
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
deps.note(`\u{1F9F9} Removed ${result.removed} original skill location(s). _Both skill trees are usually tracked by git \u2014 \`git checkout\` brings them back if this was wrong._`);
|
|
530
|
-
} else result.declined.push("removing the originals");
|
|
531
|
-
}
|
|
532
|
-
if (copied.length && deps.assignSkills) {
|
|
533
|
-
deps.note(`Working out which roles should carry ${copied.length} newly installed skill(s)\u2026`);
|
|
534
|
-
deps.busy?.("assigning skills", deps.models[0]);
|
|
535
|
-
try {
|
|
536
|
-
const assigned = await deps.assignSkills(copied);
|
|
537
|
-
deps.idle?.();
|
|
538
|
-
deps.note(assigned || "No role needed one of them permanently \u2014 every agent can still fetch them on demand.");
|
|
539
|
-
} catch (e) {
|
|
540
|
-
deps.idle?.();
|
|
541
|
-
deps.note(`Skill assignment did not run (${e instanceof Error ? e.message : String(e)}) \u2014 run \`/roles adjust\` to do it.`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
} else result.declined.push("skills");
|
|
294
|
+
const before = out.length;
|
|
295
|
+
await push("skill", tool, join(skillsDir, e.name, "SKILL.md"), `${rel}/${e.name}`);
|
|
296
|
+
if (out.length > before) seenSkills.add(e.name);
|
|
297
|
+
}
|
|
545
298
|
}
|
|
546
|
-
const mcp
|
|
547
|
-
|
|
548
|
-
deps.note(`**${mcp.length} MCP server(s)** are configured for the other tool: ${mcp.map((m) => `\`${m}\``).join(", ")}.
|
|
549
|
-
|
|
550
|
-
_These are not imported automatically \u2014 add the ones you want under \`mcp\` in your config, and mark a read-only server \`"readOnly": true\` so every agent can use it._`);
|
|
299
|
+
for (const [file, tool] of [[".mcp.json", "Claude Code"], [".cursor/mcp.json", "Cursor"], [".vscode/mcp.json", "VS Code"]]) {
|
|
300
|
+
await push("mcp", tool, join(cwd, file), file);
|
|
551
301
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
const already = (await loadMigrated(deps.cwd))?.files ?? [];
|
|
555
|
-
const unrecorded = migratedRules.filter((f) => !already.includes(f));
|
|
556
|
-
const stop = result.rules > 0 ? unrecorded.length > 0 : unrecorded.length > 0 && countKind(deps.memStore, "rule") > 0 && /^y/i.test(await deps.ask(
|
|
557
|
-
`${unrecorded.map((f) => `\`${f}\``).join(", ")} ${unrecorded.length === 1 ? "was" : "were"} left in place, and this project already has rules in memory \u2014 so ${unrecorded.length === 1 ? "it was" : "they were"} most likely migrated already. Stop reading ${unrecorded.length === 1 ? "it" : "them"} as this project's rules?`,
|
|
558
|
-
{ options: [
|
|
559
|
-
{ label: "Yes \u2014 the rules are in memory", description: "An agent that opens the file is told where the rules went" },
|
|
560
|
-
{ label: "No \u2014 keep reading them", description: "The file stays a source of rules alongside memory" }
|
|
561
|
-
] }
|
|
562
|
-
));
|
|
563
|
-
if (stop) {
|
|
564
|
-
await recordMigrated(deps.cwd, unrecorded).catch(() => {
|
|
565
|
-
});
|
|
566
|
-
deps.note(`\u{1F512} ${unrecorded.map((f) => `\`${f}\``).join(", ")} ${unrecorded.length === 1 ? "is" : "are"} no longer read as rules \u2014 their content is in this project's memory, and an agent that opens one is told where the rules went.`);
|
|
567
|
-
}
|
|
302
|
+
for (const settings of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
303
|
+
await push("mcp", "Claude Code", join(cwd, settings), settings);
|
|
568
304
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const names = /* @__PURE__ */ new Set();
|
|
573
|
-
for (const f of findings.filter((x) => x.kind === "mcp")) {
|
|
574
|
-
try {
|
|
575
|
-
const raw = f.text ?? await readFile2(f.path, "utf8");
|
|
576
|
-
const parsed = JSON.parse(raw);
|
|
577
|
-
for (const key of Object.keys(parsed.mcpServers ?? parsed.servers ?? {})) names.add(key);
|
|
578
|
-
} catch {
|
|
305
|
+
for (const [dir, kind] of [[".claude/agents", "agent"], [".claude/commands", "command"]]) {
|
|
306
|
+
for (const name of await listDir(join(cwd, dir))) {
|
|
307
|
+
if (name.endsWith(".md")) await push(kind, "Claude Code", join(cwd, dir, name), `${dir}/${name}`);
|
|
579
308
|
}
|
|
580
309
|
}
|
|
581
|
-
return
|
|
310
|
+
return out;
|
|
582
311
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
312
|
+
function summarize(findings) {
|
|
313
|
+
if (!findings.length) return "No configuration from another coding tool was found in this project.";
|
|
314
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const f of findings) {
|
|
316
|
+
const list = byKind.get(f.kind);
|
|
317
|
+
if (list) list.push(f);
|
|
318
|
+
else byKind.set(f.kind, [f]);
|
|
589
319
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
320
|
+
const LABEL = {
|
|
321
|
+
rules: "Instruction files",
|
|
322
|
+
memory: "Remembered facts",
|
|
323
|
+
skill: "Skills",
|
|
324
|
+
mcp: "MCP / settings",
|
|
325
|
+
agent: "Subagents",
|
|
326
|
+
command: "Slash commands"
|
|
327
|
+
};
|
|
328
|
+
const rows = [];
|
|
329
|
+
for (const kind of ["rules", "memory", "skill", "mcp", "agent", "command"]) {
|
|
330
|
+
const list = byKind.get(kind);
|
|
331
|
+
if (!list?.length) continue;
|
|
332
|
+
const tools = [...new Set(list.map((f) => f.tool))].join(", ");
|
|
333
|
+
const files = list.slice(0, 4).map((f) => `\`${f.label}\``).join(", ");
|
|
334
|
+
const more = list.length > 4 ? ` +${list.length - 4} more` : "";
|
|
335
|
+
rows.push(`- **${LABEL[kind]}** (${list.length}, from ${tools}): ${files}${more}`);
|
|
595
336
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
_Declined: ${r.declined.join(", ")} \u2014 re-run \`/migrate\` to revisit._` : "";
|
|
599
|
-
const left = r.skipped ? `
|
|
600
|
-
|
|
601
|
-
${r.skipped} instruction(s) were left behind as tool-specific.` : "";
|
|
602
|
-
const head = r.failedBatches ? "**Migration finished with failures.**" : "**Migration complete.**";
|
|
603
|
-
return `${head}
|
|
604
|
-
${bits.map((b) => `- ${b}`).join("\n")}${left}${declined}${failed}`;
|
|
337
|
+
return rows.join("\n");
|
|
605
338
|
}
|
|
339
|
+
function hasAnything(findings) {
|
|
340
|
+
return findings.some((f) => f.kind === "rules" || f.kind === "memory" || f.kind === "skill" || f.kind === "mcp");
|
|
341
|
+
}
|
|
342
|
+
|
|
606
343
|
export {
|
|
607
|
-
|
|
608
|
-
|
|
344
|
+
MAX_CHUNK_CHARS,
|
|
345
|
+
extractAll,
|
|
346
|
+
groupForReview,
|
|
347
|
+
MAX_RULES,
|
|
348
|
+
consolidateRules,
|
|
349
|
+
readFinding,
|
|
350
|
+
discover,
|
|
351
|
+
summarize,
|
|
352
|
+
hasAnything
|
|
609
353
|
};
|