@thesmurph/agentlink 0.1.0 → 0.2.0
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/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/dist/cli.js +54 -13
- package/dist/harnesses.js +26 -6
- package/package.json +1 -1
- package/src/cli.ts +60 -15
- package/src/harnesses.ts +34 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Corrects the path table against the code each harness actually ships, checked
|
|
6
|
+
with `scripts/verify-paths.mjs`. Paths now come from evidence rather than from
|
|
7
|
+
documentation that may describe an older layout.
|
|
8
|
+
|
|
9
|
+
- Qoder and Grok read `.agents/skills` themselves, so they need no link. Both
|
|
10
|
+
were aliases; Qoder's own directory holds settings and repowiki, not skills,
|
|
11
|
+
and Grok's binary states that it scans `.agents/skills/` at every tier.
|
|
12
|
+
- opencode's `.opencode/skills` is confirmed in its shipped binary.
|
|
13
|
+
- Kilo Code's skills path is now reported as unknown. `.claude/skills` came from
|
|
14
|
+
a third-party table and appears nowhere in its 195 MB binary, so agentlink
|
|
15
|
+
stopped writing links there rather than guessing.
|
|
16
|
+
- Factory Droid's home-directory instructions file has no evidence behind it and
|
|
17
|
+
is flagged unverified, though both of its skills paths are confirmed.
|
|
18
|
+
- Claude Code, Codex, Pi, Copilot, Qwen and Mastra Code rows were confirmed as
|
|
19
|
+
they stood.
|
|
20
|
+
|
|
21
|
+
## 0.1.2
|
|
22
|
+
|
|
23
|
+
- `list` explains what `unverified` means: the vendor's docs do not confirm that
|
|
24
|
+
path, so the link may sit where nothing reads it. It is not an error.
|
|
25
|
+
- An explicit `--harnesses`, `--all` or `--detected` sets the saved selection, so
|
|
26
|
+
sync now reports what it added or dropped instead of changing it quietly.
|
|
27
|
+
`sync --json` gains `selection.added` and `selection.removed`.
|
|
28
|
+
- Colour is written only to a TTY, and `NO_COLOR` is honoured, so piped output
|
|
29
|
+
and CI logs contain no escape codes.
|
|
30
|
+
|
|
31
|
+
## 0.1.1
|
|
32
|
+
|
|
33
|
+
- `list` shows what is linked rather than only what is installed. A `✓` means
|
|
34
|
+
agentlink is linking that harness; a trailing note appears when a harness is
|
|
35
|
+
installed but not linked, or linked but not installed. `list --json` gains
|
|
36
|
+
`selected` and a top-level `linked` array.
|
|
37
|
+
- Fixed: `list` printed one mark for two different facts, so a deselected
|
|
38
|
+
harness looked selected. The selection itself was always saved correctly.
|
|
39
|
+
|
|
3
40
|
## 0.1.0
|
|
4
41
|
|
|
5
42
|
First release.
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ agentlink init create AGENTS.md + .agents/skills, migrate, link
|
|
|
32
32
|
agentlink sync re-link after adding or moving a skill
|
|
33
33
|
agentlink fix fold stray real copies into .agents, then link
|
|
34
34
|
agentlink select change which harnesses are linked
|
|
35
|
-
agentlink list
|
|
35
|
+
agentlink list what is linked, where each harness reads from, what is installed
|
|
36
36
|
agentlink doctor drift, duplicates, invalid skills, broken links
|
|
37
37
|
agentlink adopt move an existing CLAUDE.md or GEMINI.md into AGENTS.md
|
|
38
38
|
agentlink unlink remove the links agentlink created
|
package/dist/cli.js
CHANGED
|
@@ -13,13 +13,20 @@ import { apply, mergeState, plan, pruneStale, readState, unlink, writeState } fr
|
|
|
13
13
|
import { resolveScope } from "./scope.js";
|
|
14
14
|
import { selectMany } from "./ui.js";
|
|
15
15
|
const ESC = String.fromCharCode(27);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
16
|
+
// Colour only when something is reading it: a TTY, and not opted out. Piped
|
|
17
|
+
// output and CI logs stay free of escape codes.
|
|
18
|
+
const useColor = process.env.NO_COLOR === undefined &&
|
|
19
|
+
process.env.FORCE_COLOR !== "0" &&
|
|
20
|
+
(process.env.FORCE_COLOR !== undefined || process.stdout.isTTY === true);
|
|
21
|
+
const paint = (code) => (useColor ? `${ESC}[${code}m` : "");
|
|
22
|
+
const BOLD = paint("1");
|
|
23
|
+
const DIM = paint("2");
|
|
24
|
+
const RED = paint("31");
|
|
25
|
+
const GREEN = paint("32");
|
|
26
|
+
const YELLOW = paint("33");
|
|
27
|
+
const CYAN = paint("36");
|
|
28
|
+
const RESET = paint("0");
|
|
29
|
+
;
|
|
23
30
|
const COMMANDS = ["init", "sync", "select", "fix", "list", "doctor", "adopt", "unlink", "help", "version"];
|
|
24
31
|
function parseArgs(argv) {
|
|
25
32
|
const options = {
|
|
@@ -258,6 +265,11 @@ async function syncLinks(paths, chosen, options, context) {
|
|
|
258
265
|
pruned,
|
|
259
266
|
native: linkPlan.native,
|
|
260
267
|
unknown: linkPlan.unknown,
|
|
268
|
+
selection: {
|
|
269
|
+
harnesses: chosen.map((h) => h.id),
|
|
270
|
+
added: chosen.map((h) => h.id).filter((id) => !previous.harnesses.includes(id)),
|
|
271
|
+
removed: previous.harnesses.filter((id) => !chosen.some((h) => h.id === id)),
|
|
272
|
+
},
|
|
261
273
|
aliases: linkPlan.aliases,
|
|
262
274
|
skills: linkPlan.skillsFound,
|
|
263
275
|
conflicts: conflicts.map((fix) => fix.target),
|
|
@@ -274,6 +286,11 @@ async function syncLinks(paths, chosen, options, context) {
|
|
|
274
286
|
}
|
|
275
287
|
const where = paths.scope === "global" ? "~" : path.basename(paths.root);
|
|
276
288
|
process.stdout.write(`\n${BOLD}agentlink${RESET} ${DIM}${where} · ${chosen.length} harness${chosen.length === 1 ? "" : "es"}${options.dryRun ? " · dry run" : ""}${RESET}\n`);
|
|
289
|
+
// An explicit --harnesses/--all/--detected changes the saved selection, so
|
|
290
|
+
// say which harnesses that added or dropped rather than letting it pass.
|
|
291
|
+
const selection = selectionChange(previous.harnesses, chosen);
|
|
292
|
+
if (selection)
|
|
293
|
+
step("select", selection);
|
|
277
294
|
for (const result of results) {
|
|
278
295
|
const symbol = result.state === "skipped"
|
|
279
296
|
? `${YELLOW}!${RESET}`
|
|
@@ -326,9 +343,12 @@ async function syncLinks(paths, chosen, options, context) {
|
|
|
326
343
|
process.exit(1);
|
|
327
344
|
}
|
|
328
345
|
async function runList(paths, options) {
|
|
346
|
+
const state = readState(paths);
|
|
347
|
+
const selected = new Set(state.harnesses);
|
|
329
348
|
const rows = detectAll().map(({ harness, installed, reasons }) => ({
|
|
330
349
|
id: harness.id,
|
|
331
350
|
label: harness.label,
|
|
351
|
+
selected: selected.has(harness.id),
|
|
332
352
|
installed,
|
|
333
353
|
reasons,
|
|
334
354
|
instructions: describe(harness.instructions[paths.scope]),
|
|
@@ -338,18 +358,28 @@ async function runList(paths, options) {
|
|
|
338
358
|
source: harness.source,
|
|
339
359
|
}));
|
|
340
360
|
if (options.json) {
|
|
341
|
-
process.stdout.write(`${JSON.stringify({ scope: paths.scope, harnesses: rows }, null, 2)}\n`);
|
|
361
|
+
process.stdout.write(`${JSON.stringify({ scope: paths.scope, root: paths.root, linked: [...selected], harnesses: rows }, null, 2)}\n`);
|
|
342
362
|
return;
|
|
343
363
|
}
|
|
344
364
|
const width = Math.max(...rows.map((row) => row.label.length));
|
|
345
|
-
|
|
365
|
+
const installedCount = rows.filter((row) => row.installed).length;
|
|
366
|
+
process.stdout.write(`\n${BOLD}harnesses${RESET} ${DIM}· scope: ${paths.scope} · ${selected.size} linked · ${installedCount} installed here${RESET}\n\n`);
|
|
346
367
|
for (const row of rows) {
|
|
347
|
-
|
|
368
|
+
// The leading mark is the tool's own state: what is actually linked.
|
|
369
|
+
// Availability is only called out when it is surprising.
|
|
370
|
+
const mark = row.selected ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
371
|
+
const note = row.selected && !row.installed
|
|
372
|
+
? ` ${YELLOW}selected, not installed${RESET}`
|
|
373
|
+
: !row.selected && row.installed
|
|
374
|
+
? ` ${DIM}installed, not linked${RESET}`
|
|
375
|
+
: "";
|
|
348
376
|
const flag = !row.instructionsVerified || !row.skillsVerified ? ` ${YELLOW}unverified${RESET}` : "";
|
|
349
|
-
process.stdout.write(` ${mark} ${row.label.padEnd(width)} ${DIM}instructions${RESET} ${row.instructions} ${DIM}skills${RESET} ${row.skills}${flag}\n`);
|
|
377
|
+
process.stdout.write(` ${mark} ${row.label.padEnd(width)} ${DIM}instructions${RESET} ${row.instructions} ${DIM}skills${RESET} ${row.skills}${flag}${note}\n`);
|
|
350
378
|
}
|
|
351
|
-
process.stdout.write(`\n ${GREEN}
|
|
352
|
-
` ${DIM}
|
|
379
|
+
process.stdout.write(`\n ${GREEN}✓${RESET} linked ${DIM}·${RESET} not linked ${DIM}change with \`agentlink select\`${RESET}\n` +
|
|
380
|
+
` ${DIM}native = harness reads AGENTS.md / .agents/skills itself${RESET}\n` +
|
|
381
|
+
` ${YELLOW}unverified${RESET} ${DIM}= the vendor's docs do not confirm that path, so the link may sit where nothing reads it.${RESET}\n` +
|
|
382
|
+
` ${DIM}It is not an error. \`list --json\` prints the source URL for every endpoint.${RESET}\n`);
|
|
353
383
|
}
|
|
354
384
|
function describe(endpoint) {
|
|
355
385
|
if (endpoint.native)
|
|
@@ -511,6 +541,17 @@ function dedupe(harnesses) {
|
|
|
511
541
|
function step(verb, message) {
|
|
512
542
|
process.stdout.write(` ${GREEN}✓${RESET} ${DIM}${verb.padEnd(7)}${RESET}${message}\n`);
|
|
513
543
|
}
|
|
544
|
+
/** Describe a change to the saved selection, or null when nothing changed. */
|
|
545
|
+
function selectionChange(before, chosen) {
|
|
546
|
+
const after = chosen.map((harness) => harness.id);
|
|
547
|
+
const added = after.filter((id) => !before.includes(id));
|
|
548
|
+
const removed = before.filter((id) => !after.includes(id));
|
|
549
|
+
if (added.length === 0 && removed.length === 0)
|
|
550
|
+
return null;
|
|
551
|
+
// The first run has nothing to compare against; the list above already says it.
|
|
552
|
+
const parts = [added.length ? `+${added.join(" ")}` : "", removed.length ? `-${removed.join(" ")}` : ""];
|
|
553
|
+
return `${after.length} harness${after.length === 1 ? "" : "es"}${before.length ? ` (was ${before.length})` : ""}: ${parts.filter(Boolean).join(" ")}`;
|
|
554
|
+
}
|
|
514
555
|
function version() {
|
|
515
556
|
try {
|
|
516
557
|
const file = fileURLToPath(new URL("../package.json", import.meta.url));
|
package/dist/harnesses.js
CHANGED
|
@@ -27,6 +27,7 @@ export const HARNESSES = [
|
|
|
27
27
|
label: "Claude Code",
|
|
28
28
|
bins: ["claude"],
|
|
29
29
|
configRoot: ".claude",
|
|
30
|
+
npmPackage: "@anthropic-ai/claude-code",
|
|
30
31
|
// Claude Code reads CLAUDE.md, not AGENTS.md, and never scans .agents/skills.
|
|
31
32
|
instructions: scopes({ native: false, alias: "CLAUDE.md", note: "Claude Code has no AGENTS.md fallback" }, { native: false, alias: ".claude/CLAUDE.md" }),
|
|
32
33
|
skills: scopes({ native: false, alias: ".claude/skills" }, { native: false, alias: ".claude/skills" }),
|
|
@@ -37,6 +38,7 @@ export const HARNESSES = [
|
|
|
37
38
|
label: "OpenAI Codex CLI",
|
|
38
39
|
bins: ["codex"],
|
|
39
40
|
configRoot: ".codex",
|
|
41
|
+
npmPackage: "@openai/codex",
|
|
40
42
|
// Codex reads AGENTS.md in the repo and ~/.codex/AGENTS.md globally.
|
|
41
43
|
instructions: scopes({ native: true }, { native: false, alias: ".codex/AGENTS.md" }),
|
|
42
44
|
// Codex's user-level skills directory is $HOME/.agents/skills: native both ways.
|
|
@@ -48,6 +50,7 @@ export const HARNESSES = [
|
|
|
48
50
|
label: "Pi",
|
|
49
51
|
bins: ["pi"],
|
|
50
52
|
configRoot: ".pi",
|
|
53
|
+
npmPackage: "@earendil-works/pi-coding-agent",
|
|
51
54
|
instructions: scopes({ native: true }, { native: false, alias: ".pi/agent/AGENTS.md" }),
|
|
52
55
|
// Pi reads ~/.agents/skills and .agents/skills directly.
|
|
53
56
|
skills: scopes(NATIVE, NATIVE),
|
|
@@ -58,6 +61,7 @@ export const HARNESSES = [
|
|
|
58
61
|
label: "Oh My Pi (omp)",
|
|
59
62
|
bins: ["omp"],
|
|
60
63
|
configRoot: ".omp",
|
|
64
|
+
npmPackage: "@oh-my-pi/pi-coding-agent",
|
|
61
65
|
instructions: scopes({ native: true, note: "standalone AGENTS.md via the agents-md provider" }, { native: false, alias: ".omp/agent/AGENTS.md" }),
|
|
62
66
|
// omp scans its own .omp/skills trees; it does not read .agents/skills.
|
|
63
67
|
skills: scopes({ native: false, alias: ".omp/skills" }, { native: false, alias: ".omp/agent/skills" }),
|
|
@@ -68,6 +72,7 @@ export const HARNESSES = [
|
|
|
68
72
|
label: "GitHub Copilot CLI",
|
|
69
73
|
bins: ["copilot"],
|
|
70
74
|
configRoot: ".copilot",
|
|
75
|
+
npmPackage: "@github/copilot",
|
|
71
76
|
// Copilot CLI reads AGENTS.md everywhere, but its *user* instructions live
|
|
72
77
|
// in a differently named file.
|
|
73
78
|
instructions: scopes({ native: true }, { native: false, alias: ".copilot/copilot-instructions.md" }),
|
|
@@ -88,8 +93,11 @@ export const HARNESSES = [
|
|
|
88
93
|
label: "opencode",
|
|
89
94
|
bins: ["opencode"],
|
|
90
95
|
configRoot: ".config/opencode",
|
|
96
|
+
npmPackage: "opencode-ai",
|
|
91
97
|
instructions: scopes({ native: true }, { native: false, alias: ".config/opencode/AGENTS.md" }),
|
|
92
|
-
|
|
98
|
+
// .opencode/skills confirmed in the shipped binary; the home-directory
|
|
99
|
+
// equivalent is not a literal anywhere in it.
|
|
100
|
+
skills: scopes({ native: false, alias: ".opencode/skills" }, { native: false, alias: ".config/opencode/skills", verified: false }),
|
|
93
101
|
source: "https://opencode.ai/docs/rules/",
|
|
94
102
|
},
|
|
95
103
|
{
|
|
@@ -97,6 +105,7 @@ export const HARNESSES = [
|
|
|
97
105
|
label: "Qwen Code",
|
|
98
106
|
bins: ["qwen"],
|
|
99
107
|
configRoot: ".qwen",
|
|
108
|
+
npmPackage: "@qwen-code/qwen-code",
|
|
100
109
|
instructions: scopes({ native: true, note: "QWEN.md is the legacy name" }, { native: false, alias: ".qwen/AGENTS.md", note: "not documented by Qwen", verified: false }),
|
|
101
110
|
skills: scopes({ native: false, alias: ".qwen/skills" }, { native: false, alias: ".qwen/skills" }),
|
|
102
111
|
source: "https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/",
|
|
@@ -115,9 +124,13 @@ export const HARNESSES = [
|
|
|
115
124
|
label: "Kilo Code",
|
|
116
125
|
bins: ["kilo"],
|
|
117
126
|
configRoot: ".config/kilo",
|
|
127
|
+
npmPackage: "@kilocode/cli",
|
|
118
128
|
instructions: scopes({ native: true }, { native: false, alias: ".config/kilo/AGENTS.md", note: "not documented by Kilo" }),
|
|
119
|
-
//
|
|
120
|
-
|
|
129
|
+
// No skills path survived inspection of the shipped binary: its config
|
|
130
|
+
// directory holds command/, themes/ and config files, and .claude/skills
|
|
131
|
+
// (a third-party claim) appears nowhere in 195 MB. Reported as unknown
|
|
132
|
+
// rather than linked into a directory nothing reads.
|
|
133
|
+
skills: scopes({ native: false, note: "unresolved: not found in the shipped binary" }, { native: false, note: "unresolved: not found in the shipped binary" }),
|
|
121
134
|
source: "https://github.com/intellectronica/ruler#skills-support-experimental",
|
|
122
135
|
},
|
|
123
136
|
{
|
|
@@ -125,7 +138,8 @@ export const HARNESSES = [
|
|
|
125
138
|
label: "Factory Droid",
|
|
126
139
|
bins: ["droid"],
|
|
127
140
|
configRoot: ".factory",
|
|
128
|
-
|
|
141
|
+
npmPackage: "@factory/cli",
|
|
142
|
+
instructions: scopes({ native: true, note: "AGENTS.md may also live in the home directory" }, { native: false, alias: ".factory/AGENTS.md", note: "not found in the shipped binary", verified: false }),
|
|
129
143
|
skills: scopes({ native: false, alias: ".factory/skills" }, { native: false, alias: ".factory/skills" }),
|
|
130
144
|
source: "https://docs.factory.ai/harness/skills",
|
|
131
145
|
},
|
|
@@ -143,6 +157,7 @@ export const HARNESSES = [
|
|
|
143
157
|
label: "Mastra Code",
|
|
144
158
|
bins: ["mastracode"],
|
|
145
159
|
configRoot: ".mastracode",
|
|
160
|
+
npmPackage: "mastracode",
|
|
146
161
|
instructions: scopes({ native: true }, { native: false, alias: ".mastracode/AGENTS.md", note: "not documented by Mastra", verified: false }),
|
|
147
162
|
// Mastra Code lists .agents/skills as a project source and Agent Skills
|
|
148
163
|
// spec compatibility, so both scopes are native.
|
|
@@ -155,7 +170,9 @@ export const HARNESSES = [
|
|
|
155
170
|
bins: ["grok"],
|
|
156
171
|
configRoot: ".grok",
|
|
157
172
|
instructions: scopes({ native: true, note: "reads AGENTS.md, CLAUDE.md, AGENT.md" }, { native: false, alias: ".grok/AGENTS.md", note: "not documented by Grok", verified: false }),
|
|
158
|
-
|
|
173
|
+
// The binary's embedded documentation: "Grok also scans `.agents/skills/`
|
|
174
|
+
// (and `commands/`) at each tier (alongside `.grok/`)".
|
|
175
|
+
skills: scopes(NATIVE, NATIVE),
|
|
159
176
|
source: "https://docs.x.ai/docs/grok-cli/skills",
|
|
160
177
|
},
|
|
161
178
|
{
|
|
@@ -163,8 +180,11 @@ export const HARNESSES = [
|
|
|
163
180
|
label: "Qoder CLI",
|
|
164
181
|
bins: ["qodercli", "qoder"],
|
|
165
182
|
configRoot: ".qoder",
|
|
183
|
+
npmPackage: "@qoder-ai/qodercli",
|
|
166
184
|
instructions: scopes({ native: true, note: "configurable via context.fileName" }, { native: false, alias: ".qoder/AGENTS.md", note: "not documented by Qoder", verified: false }),
|
|
167
|
-
|
|
185
|
+
// Confirmed in the shipped bundle: .agents/skills is present, .qoder/skills
|
|
186
|
+
// is not. Its own config directory holds settings and repowiki, not skills.
|
|
187
|
+
skills: scopes(NATIVE, NATIVE),
|
|
168
188
|
source: "https://docs.qoder.com/cli/Skills",
|
|
169
189
|
},
|
|
170
190
|
{
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -14,13 +14,20 @@ import { resolveScope, type Scope, type ScopePaths } from "./scope.js";
|
|
|
14
14
|
import { selectMany, type Choice } from "./ui.js";
|
|
15
15
|
|
|
16
16
|
const ESC = String.fromCharCode(27);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
17
|
+
// Colour only when something is reading it: a TTY, and not opted out. Piped
|
|
18
|
+
// output and CI logs stay free of escape codes.
|
|
19
|
+
const useColor =
|
|
20
|
+
process.env.NO_COLOR === undefined &&
|
|
21
|
+
process.env.FORCE_COLOR !== "0" &&
|
|
22
|
+
(process.env.FORCE_COLOR !== undefined || process.stdout.isTTY === true);
|
|
23
|
+
const paint = (code: string): string => (useColor ? `${ESC}[${code}m` : "");
|
|
24
|
+
const BOLD = paint("1");
|
|
25
|
+
const DIM = paint("2");
|
|
26
|
+
const RED = paint("31");
|
|
27
|
+
const GREEN = paint("32");
|
|
28
|
+
const YELLOW = paint("33");
|
|
29
|
+
const CYAN = paint("36");
|
|
30
|
+
const RESET = paint("0");;
|
|
24
31
|
|
|
25
32
|
interface Options {
|
|
26
33
|
command: string;
|
|
@@ -298,6 +305,11 @@ async function syncLinks(
|
|
|
298
305
|
pruned,
|
|
299
306
|
native: linkPlan.native,
|
|
300
307
|
unknown: linkPlan.unknown,
|
|
308
|
+
selection: {
|
|
309
|
+
harnesses: chosen.map((h) => h.id),
|
|
310
|
+
added: chosen.map((h) => h.id).filter((id) => !previous.harnesses.includes(id)),
|
|
311
|
+
removed: previous.harnesses.filter((id) => !chosen.some((h) => h.id === id)),
|
|
312
|
+
},
|
|
301
313
|
aliases: linkPlan.aliases,
|
|
302
314
|
skills: linkPlan.skillsFound,
|
|
303
315
|
conflicts: conflicts.map((fix) => fix.target),
|
|
@@ -321,6 +333,11 @@ async function syncLinks(
|
|
|
321
333
|
`\n${BOLD}agentlink${RESET} ${DIM}${where} · ${chosen.length} harness${chosen.length === 1 ? "" : "es"}${options.dryRun ? " · dry run" : ""}${RESET}\n`,
|
|
322
334
|
);
|
|
323
335
|
|
|
336
|
+
// An explicit --harnesses/--all/--detected changes the saved selection, so
|
|
337
|
+
// say which harnesses that added or dropped rather than letting it pass.
|
|
338
|
+
const selection = selectionChange(previous.harnesses, chosen);
|
|
339
|
+
if (selection) step("select", selection);
|
|
340
|
+
|
|
324
341
|
for (const result of results) {
|
|
325
342
|
const symbol =
|
|
326
343
|
result.state === "skipped"
|
|
@@ -388,9 +405,12 @@ async function syncLinks(
|
|
|
388
405
|
}
|
|
389
406
|
|
|
390
407
|
async function runList(paths: ScopePaths, options: Options): Promise<void> {
|
|
408
|
+
const state = readState(paths);
|
|
409
|
+
const selected = new Set(state.harnesses);
|
|
391
410
|
const rows = detectAll().map(({ harness, installed, reasons }) => ({
|
|
392
411
|
id: harness.id,
|
|
393
412
|
label: harness.label,
|
|
413
|
+
selected: selected.has(harness.id),
|
|
394
414
|
installed,
|
|
395
415
|
reasons,
|
|
396
416
|
instructions: describe(harness.instructions[paths.scope]),
|
|
@@ -401,25 +421,37 @@ async function runList(paths: ScopePaths, options: Options): Promise<void> {
|
|
|
401
421
|
}));
|
|
402
422
|
|
|
403
423
|
if (options.json) {
|
|
404
|
-
process.stdout.write(
|
|
424
|
+
process.stdout.write(
|
|
425
|
+
`${JSON.stringify({ scope: paths.scope, root: paths.root, linked: [...selected], harnesses: rows }, null, 2)}\n`,
|
|
426
|
+
);
|
|
405
427
|
return;
|
|
406
428
|
}
|
|
407
429
|
|
|
408
430
|
const width = Math.max(...rows.map((row) => row.label.length));
|
|
431
|
+
const installedCount = rows.filter((row) => row.installed).length;
|
|
409
432
|
process.stdout.write(
|
|
410
|
-
`\n${BOLD}harnesses${RESET} ${DIM}· scope: ${paths.scope}${RESET}\n\n`,
|
|
433
|
+
`\n${BOLD}harnesses${RESET} ${DIM}· scope: ${paths.scope} · ${selected.size} linked · ${installedCount} installed here${RESET}\n\n`,
|
|
411
434
|
);
|
|
412
435
|
for (const row of rows) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
436
|
+
// The leading mark is the tool's own state: what is actually linked.
|
|
437
|
+
// Availability is only called out when it is surprising.
|
|
438
|
+
const mark = row.selected ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
439
|
+
const note =
|
|
440
|
+
row.selected && !row.installed
|
|
441
|
+
? ` ${YELLOW}selected, not installed${RESET}`
|
|
442
|
+
: !row.selected && row.installed
|
|
443
|
+
? ` ${DIM}installed, not linked${RESET}`
|
|
444
|
+
: "";
|
|
445
|
+
const flag = !row.instructionsVerified || !row.skillsVerified ? ` ${YELLOW}unverified${RESET}` : "";
|
|
416
446
|
process.stdout.write(
|
|
417
|
-
` ${mark} ${row.label.padEnd(width)} ${DIM}instructions${RESET} ${row.instructions} ${DIM}skills${RESET} ${row.skills}${flag}\n`,
|
|
447
|
+
` ${mark} ${row.label.padEnd(width)} ${DIM}instructions${RESET} ${row.instructions} ${DIM}skills${RESET} ${row.skills}${flag}${note}\n`,
|
|
418
448
|
);
|
|
419
449
|
}
|
|
420
450
|
process.stdout.write(
|
|
421
|
-
`\n ${GREEN}
|
|
422
|
-
` ${DIM}
|
|
451
|
+
`\n ${GREEN}✓${RESET} linked ${DIM}·${RESET} not linked ${DIM}change with \`agentlink select\`${RESET}\n` +
|
|
452
|
+
` ${DIM}native = harness reads AGENTS.md / .agents/skills itself${RESET}\n` +
|
|
453
|
+
` ${YELLOW}unverified${RESET} ${DIM}= the vendor's docs do not confirm that path, so the link may sit where nothing reads it.${RESET}\n` +
|
|
454
|
+
` ${DIM}It is not an error. \`list --json\` prints the source URL for every endpoint.${RESET}\n`,
|
|
423
455
|
);
|
|
424
456
|
}
|
|
425
457
|
|
|
@@ -604,6 +636,19 @@ function step(verb: string, message: string): void {
|
|
|
604
636
|
process.stdout.write(` ${GREEN}✓${RESET} ${DIM}${verb.padEnd(7)}${RESET}${message}\n`);
|
|
605
637
|
}
|
|
606
638
|
|
|
639
|
+
/** Describe a change to the saved selection, or null when nothing changed. */
|
|
640
|
+
function selectionChange(before: string[], chosen: Harness[]): string | null {
|
|
641
|
+
const after = chosen.map((harness) => harness.id);
|
|
642
|
+
const added = after.filter((id) => !before.includes(id));
|
|
643
|
+
const removed = before.filter((id) => !after.includes(id));
|
|
644
|
+
if (added.length === 0 && removed.length === 0) return null;
|
|
645
|
+
// The first run has nothing to compare against; the list above already says it.
|
|
646
|
+
const parts = [added.length ? `+${added.join(" ")}` : "", removed.length ? `-${removed.join(" ")}` : ""];
|
|
647
|
+
return `${after.length} harness${after.length === 1 ? "" : "es"}${
|
|
648
|
+
before.length ? ` (was ${before.length})` : ""
|
|
649
|
+
}: ${parts.filter(Boolean).join(" ")}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
607
652
|
function version(): void {
|
|
608
653
|
try {
|
|
609
654
|
const file = fileURLToPath(new URL("../package.json", import.meta.url));
|
package/src/harnesses.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface Harness {
|
|
|
38
38
|
bins: string[];
|
|
39
39
|
/** Config directory relative to $HOME (from `herdr integration status`). */
|
|
40
40
|
configRoot: string;
|
|
41
|
+
/**
|
|
42
|
+
* The vendor's own npm package, where one exists. Used to check this table
|
|
43
|
+
* against the code that actually ships; third-party packages with similar
|
|
44
|
+
* names are deliberately absent, since inspecting the wrong artifact is how a
|
|
45
|
+
* wrong path gets into a table.
|
|
46
|
+
*/
|
|
47
|
+
npmPackage?: string;
|
|
41
48
|
instructions: Record<Scope, Endpoint>;
|
|
42
49
|
skills: Record<Scope, Endpoint>;
|
|
43
50
|
/** Primary documentation these paths were read from. */
|
|
@@ -83,6 +90,7 @@ export const HARNESSES: Harness[] = [
|
|
|
83
90
|
label: "Claude Code",
|
|
84
91
|
bins: ["claude"],
|
|
85
92
|
configRoot: ".claude",
|
|
93
|
+
npmPackage: "@anthropic-ai/claude-code",
|
|
86
94
|
// Claude Code reads CLAUDE.md, not AGENTS.md, and never scans .agents/skills.
|
|
87
95
|
instructions: scopes(
|
|
88
96
|
{ native: false, alias: "CLAUDE.md", note: "Claude Code has no AGENTS.md fallback" },
|
|
@@ -99,6 +107,7 @@ export const HARNESSES: Harness[] = [
|
|
|
99
107
|
label: "OpenAI Codex CLI",
|
|
100
108
|
bins: ["codex"],
|
|
101
109
|
configRoot: ".codex",
|
|
110
|
+
npmPackage: "@openai/codex",
|
|
102
111
|
// Codex reads AGENTS.md in the repo and ~/.codex/AGENTS.md globally.
|
|
103
112
|
instructions: scopes(
|
|
104
113
|
{ native: true },
|
|
@@ -113,6 +122,7 @@ export const HARNESSES: Harness[] = [
|
|
|
113
122
|
label: "Pi",
|
|
114
123
|
bins: ["pi"],
|
|
115
124
|
configRoot: ".pi",
|
|
125
|
+
npmPackage: "@earendil-works/pi-coding-agent",
|
|
116
126
|
instructions: scopes(
|
|
117
127
|
{ native: true },
|
|
118
128
|
{ native: false, alias: ".pi/agent/AGENTS.md" },
|
|
@@ -126,6 +136,7 @@ export const HARNESSES: Harness[] = [
|
|
|
126
136
|
label: "Oh My Pi (omp)",
|
|
127
137
|
bins: ["omp"],
|
|
128
138
|
configRoot: ".omp",
|
|
139
|
+
npmPackage: "@oh-my-pi/pi-coding-agent",
|
|
129
140
|
instructions: scopes(
|
|
130
141
|
{ native: true, note: "standalone AGENTS.md via the agents-md provider" },
|
|
131
142
|
{ native: false, alias: ".omp/agent/AGENTS.md" },
|
|
@@ -142,6 +153,7 @@ export const HARNESSES: Harness[] = [
|
|
|
142
153
|
label: "GitHub Copilot CLI",
|
|
143
154
|
bins: ["copilot"],
|
|
144
155
|
configRoot: ".copilot",
|
|
156
|
+
npmPackage: "@github/copilot",
|
|
145
157
|
// Copilot CLI reads AGENTS.md everywhere, but its *user* instructions live
|
|
146
158
|
// in a differently named file.
|
|
147
159
|
instructions: scopes(
|
|
@@ -171,12 +183,15 @@ export const HARNESSES: Harness[] = [
|
|
|
171
183
|
label: "opencode",
|
|
172
184
|
bins: ["opencode"],
|
|
173
185
|
configRoot: ".config/opencode",
|
|
186
|
+
npmPackage: "opencode-ai",
|
|
174
187
|
instructions: scopes(
|
|
175
188
|
{ native: true },
|
|
176
189
|
{ native: false, alias: ".config/opencode/AGENTS.md" },
|
|
177
190
|
),
|
|
191
|
+
// .opencode/skills confirmed in the shipped binary; the home-directory
|
|
192
|
+
// equivalent is not a literal anywhere in it.
|
|
178
193
|
skills: scopes(
|
|
179
|
-
{ native: false, alias: ".opencode/skills"
|
|
194
|
+
{ native: false, alias: ".opencode/skills" },
|
|
180
195
|
{ native: false, alias: ".config/opencode/skills", verified: false },
|
|
181
196
|
),
|
|
182
197
|
source: "https://opencode.ai/docs/rules/",
|
|
@@ -186,6 +201,7 @@ export const HARNESSES: Harness[] = [
|
|
|
186
201
|
label: "Qwen Code",
|
|
187
202
|
bins: ["qwen"],
|
|
188
203
|
configRoot: ".qwen",
|
|
204
|
+
npmPackage: "@qwen-code/qwen-code",
|
|
189
205
|
instructions: scopes(
|
|
190
206
|
{ native: true, note: "QWEN.md is the legacy name" },
|
|
191
207
|
{ native: false, alias: ".qwen/AGENTS.md", note: "not documented by Qwen", verified: false },
|
|
@@ -216,14 +232,18 @@ export const HARNESSES: Harness[] = [
|
|
|
216
232
|
label: "Kilo Code",
|
|
217
233
|
bins: ["kilo"],
|
|
218
234
|
configRoot: ".config/kilo",
|
|
235
|
+
npmPackage: "@kilocode/cli",
|
|
219
236
|
instructions: scopes(
|
|
220
237
|
{ native: true },
|
|
221
238
|
{ native: false, alias: ".config/kilo/AGENTS.md", note: "not documented by Kilo" },
|
|
222
239
|
),
|
|
223
|
-
//
|
|
240
|
+
// No skills path survived inspection of the shipped binary: its config
|
|
241
|
+
// directory holds command/, themes/ and config files, and .claude/skills
|
|
242
|
+
// (a third-party claim) appears nowhere in 195 MB. Reported as unknown
|
|
243
|
+
// rather than linked into a directory nothing reads.
|
|
224
244
|
skills: scopes(
|
|
225
|
-
{ native: false,
|
|
226
|
-
{ native: false,
|
|
245
|
+
{ native: false, note: "unresolved: not found in the shipped binary" },
|
|
246
|
+
{ native: false, note: "unresolved: not found in the shipped binary" },
|
|
227
247
|
),
|
|
228
248
|
source: "https://github.com/intellectronica/ruler#skills-support-experimental",
|
|
229
249
|
},
|
|
@@ -232,9 +252,10 @@ export const HARNESSES: Harness[] = [
|
|
|
232
252
|
label: "Factory Droid",
|
|
233
253
|
bins: ["droid"],
|
|
234
254
|
configRoot: ".factory",
|
|
255
|
+
npmPackage: "@factory/cli",
|
|
235
256
|
instructions: scopes(
|
|
236
257
|
{ native: true, note: "AGENTS.md may also live in the home directory" },
|
|
237
|
-
{ native: false, alias: ".factory/AGENTS.md" },
|
|
258
|
+
{ native: false, alias: ".factory/AGENTS.md", note: "not found in the shipped binary", verified: false },
|
|
238
259
|
),
|
|
239
260
|
skills: scopes(
|
|
240
261
|
{ native: false, alias: ".factory/skills" },
|
|
@@ -262,6 +283,7 @@ export const HARNESSES: Harness[] = [
|
|
|
262
283
|
label: "Mastra Code",
|
|
263
284
|
bins: ["mastracode"],
|
|
264
285
|
configRoot: ".mastracode",
|
|
286
|
+
npmPackage: "mastracode",
|
|
265
287
|
instructions: scopes(
|
|
266
288
|
{ native: true },
|
|
267
289
|
{ native: false, alias: ".mastracode/AGENTS.md", note: "not documented by Mastra", verified: false },
|
|
@@ -280,10 +302,9 @@ export const HARNESSES: Harness[] = [
|
|
|
280
302
|
{ native: true, note: "reads AGENTS.md, CLAUDE.md, AGENT.md" },
|
|
281
303
|
{ native: false, alias: ".grok/AGENTS.md", note: "not documented by Grok", verified: false },
|
|
282
304
|
),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
),
|
|
305
|
+
// The binary's embedded documentation: "Grok also scans `.agents/skills/`
|
|
306
|
+
// (and `commands/`) at each tier (alongside `.grok/`)".
|
|
307
|
+
skills: scopes(NATIVE, NATIVE),
|
|
287
308
|
source: "https://docs.x.ai/docs/grok-cli/skills",
|
|
288
309
|
},
|
|
289
310
|
{
|
|
@@ -291,14 +312,14 @@ export const HARNESSES: Harness[] = [
|
|
|
291
312
|
label: "Qoder CLI",
|
|
292
313
|
bins: ["qodercli", "qoder"],
|
|
293
314
|
configRoot: ".qoder",
|
|
315
|
+
npmPackage: "@qoder-ai/qodercli",
|
|
294
316
|
instructions: scopes(
|
|
295
317
|
{ native: true, note: "configurable via context.fileName" },
|
|
296
318
|
{ native: false, alias: ".qoder/AGENTS.md", note: "not documented by Qoder", verified: false },
|
|
297
319
|
),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
),
|
|
320
|
+
// Confirmed in the shipped bundle: .agents/skills is present, .qoder/skills
|
|
321
|
+
// is not. Its own config directory holds settings and repowiki, not skills.
|
|
322
|
+
skills: scopes(NATIVE, NATIVE),
|
|
302
323
|
source: "https://docs.qoder.com/cli/Skills",
|
|
303
324
|
},
|
|
304
325
|
{
|