@gethmy/mcp 3.7.0 → 3.9.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/README.md +3 -3
- package/dist/cli.js +623 -157
- package/dist/index.js +228 -97
- package/dist/lib/api-client.js +181 -16
- package/dist/lib/config.js +110 -14
- package/dist/lib/oauth-refresh.js +110 -14
- package/dist/run-hook-cli.js +54 -0
- package/package.json +2 -2
- package/src/api-client.ts +91 -1
- package/src/config.ts +262 -14
- package/src/prompt-builder.ts +1 -1
- package/src/server.ts +70 -4
- package/src/skills.ts +6 -80
- package/src/tui/agent-instructions.ts +335 -0
- package/src/tui/setup.ts +144 -63
- package/src/tui/writer.ts +118 -2
package/src/tui/setup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
existsSync,
|
|
@@ -23,7 +24,11 @@ import {
|
|
|
23
24
|
} from "../config.js";
|
|
24
25
|
import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
|
|
25
26
|
import { onboardNewUser } from "../onboard.js";
|
|
26
|
-
import { buildSkillFile
|
|
27
|
+
import { buildSkillFile } from "../skills.js";
|
|
28
|
+
import {
|
|
29
|
+
HARMONY_AGENTS_SECTION,
|
|
30
|
+
renderWorkflowPrompt,
|
|
31
|
+
} from "./agent-instructions.js";
|
|
27
32
|
import { type AgentId, detectAgents } from "./agents.js";
|
|
28
33
|
import { confirmOrDefault, shouldAssumeYes } from "./confirm.js";
|
|
29
34
|
import { runDocsStep } from "./docs.js";
|
|
@@ -61,11 +66,17 @@ export interface SetupOptions {
|
|
|
61
66
|
* keep, especially under autonomous agent runs. New tools default to prompting
|
|
62
67
|
* until someone classifies them here (safe failure mode). `--allow-all-tools`
|
|
63
68
|
* overrides this with a blanket grant.
|
|
69
|
+
*
|
|
70
|
+
* Every name here must exist in the advertised `TOOLS` object in `server.ts`.
|
|
71
|
+
* `harmony_get_card_by_short_id` sat in this list for four minor versions after
|
|
72
|
+
* 2.14.0 dropped it, writing a permission rule for a tool that can never be
|
|
73
|
+
* offered — and the same dead name was being installed into AGENTS.md, where an
|
|
74
|
+
* agent read it and had to correct us. `setup-agent-files.test.ts` now fails on
|
|
75
|
+
* a name that is not advertised, in this list and in every generated file (#1124).
|
|
64
76
|
*/
|
|
65
|
-
const SAFE_HARMONY_TOOLS = [
|
|
77
|
+
export const SAFE_HARMONY_TOOLS = [
|
|
66
78
|
// Reads
|
|
67
79
|
"harmony_get_card",
|
|
68
|
-
"harmony_get_card_by_short_id",
|
|
69
80
|
"harmony_search_cards",
|
|
70
81
|
"harmony_get_board",
|
|
71
82
|
"harmony_get_context",
|
|
@@ -77,12 +88,19 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
77
88
|
"harmony_get_comments",
|
|
78
89
|
"harmony_get_plan",
|
|
79
90
|
"harmony_list_plans",
|
|
91
|
+
"harmony_get_playbook",
|
|
92
|
+
"harmony_list_playbook",
|
|
80
93
|
"harmony_get_agent_session",
|
|
94
|
+
// The checkpoint steering poll. Absent until #1124, so every checkpoint of
|
|
95
|
+
// every run raised a prompt for a read the workflow itself prescribes.
|
|
96
|
+
"harmony_get_pending_messages",
|
|
81
97
|
"harmony_get_workspace_members",
|
|
82
98
|
"harmony_list_agents",
|
|
83
99
|
"harmony_resolve_links",
|
|
100
|
+
"harmony_suggest_relations",
|
|
84
101
|
"harmony_recall",
|
|
85
102
|
"harmony_memory_search",
|
|
103
|
+
"harmony_vault_index",
|
|
86
104
|
"harmony_generate_prompt",
|
|
87
105
|
// Routine writes
|
|
88
106
|
"harmony_create_card",
|
|
@@ -91,6 +109,7 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
91
109
|
"harmony_assign_card",
|
|
92
110
|
"harmony_create_subtask",
|
|
93
111
|
"harmony_toggle_subtask",
|
|
112
|
+
"harmony_update_subtask",
|
|
94
113
|
"harmony_add_label_to_card",
|
|
95
114
|
"harmony_remove_label_from_card",
|
|
96
115
|
"harmony_create_label",
|
|
@@ -98,6 +117,10 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
98
117
|
"harmony_update_comment",
|
|
99
118
|
"harmony_add_link_to_card",
|
|
100
119
|
"harmony_remove_link_from_card",
|
|
120
|
+
// The durable carrier for a PR link — the completion path calls it on every
|
|
121
|
+
// card that changed code, and it mirrors the card-link pair above.
|
|
122
|
+
"harmony_add_external_link",
|
|
123
|
+
"harmony_remove_external_link",
|
|
101
124
|
"harmony_start_agent_session",
|
|
102
125
|
"harmony_update_agent_progress",
|
|
103
126
|
"harmony_end_agent_session",
|
|
@@ -112,6 +135,9 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
112
135
|
"harmony_remember",
|
|
113
136
|
"harmony_relate",
|
|
114
137
|
"harmony_update_memory",
|
|
138
|
+
// A ranking counter on a recalled memory. A write, not a read — it never
|
|
139
|
+
// deletes, hides or supersedes anything.
|
|
140
|
+
"harmony_recall_feedback",
|
|
115
141
|
"harmony_process_command",
|
|
116
142
|
"harmony_sync",
|
|
117
143
|
];
|
|
@@ -401,7 +427,8 @@ export async function resolveProjectSlug(
|
|
|
401
427
|
export interface FileToWrite {
|
|
402
428
|
path: string;
|
|
403
429
|
content: string;
|
|
404
|
-
|
|
430
|
+
/** `markdown` merges a Harmony-owned section into a file the project owns. */
|
|
431
|
+
type: "text" | "json" | "toml" | "markdown";
|
|
405
432
|
tomlSection?: string;
|
|
406
433
|
mode?: number;
|
|
407
434
|
}
|
|
@@ -512,64 +539,15 @@ async function getAgentFiles(
|
|
|
512
539
|
}
|
|
513
540
|
|
|
514
541
|
case "codex": {
|
|
515
|
-
// AGENTS.md
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
## Agent identity — always identify as yourself
|
|
521
|
-
|
|
522
|
-
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
523
|
-
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
524
|
-
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
525
|
-
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
526
|
-
|
|
527
|
-
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
528
|
-
- \`agentName\` — its human-readable name
|
|
529
|
-
|
|
530
|
-
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
531
|
-
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
532
|
-
than borrowing the closest entry.
|
|
533
|
-
|
|
534
|
-
## Starting Work on a Card
|
|
535
|
-
|
|
536
|
-
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
537
|
-
|
|
538
|
-
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
539
|
-
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
540
|
-
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
541
|
-
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
542
|
-
\`agentName\` (see "Agent identity" above)
|
|
543
|
-
5. Show the card details to the user
|
|
544
|
-
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
545
|
-
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
546
|
-
8. When done, call \`harmony_end_agent_session\` and move to "Review"
|
|
547
|
-
|
|
548
|
-
## Auto-Detect Card for Implementation Tasks
|
|
549
|
-
|
|
550
|
-
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
551
|
-
|
|
552
|
-
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
553
|
-
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
554
|
-
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
555
|
-
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
556
|
-
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
557
|
-
|
|
558
|
-
Skip if: work was already started with a card reference, or no matching card exists.
|
|
559
|
-
|
|
560
|
-
## Available Harmony Tools
|
|
561
|
-
|
|
562
|
-
- \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
|
|
563
|
-
- \`harmony_move_card\` - Move cards between columns
|
|
564
|
-
- \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
|
|
565
|
-
- \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
|
|
566
|
-
- \`harmony_get_board\` - Get board state
|
|
567
|
-
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
568
|
-
`;
|
|
542
|
+
// The Harmony section of AGENTS.md, written as type "markdown" so it is
|
|
543
|
+
// MERGED into whatever the project already has: the docs-step scaffold
|
|
544
|
+
// written earlier in this same run, or a file the user maintains by hand.
|
|
545
|
+
// A plain text write destroyed both — setup forces writes on a fresh
|
|
546
|
+
// install, and this push lands after the scaffold's (#1124).
|
|
569
547
|
files.push({
|
|
570
548
|
path: join(cwd, "AGENTS.md"),
|
|
571
|
-
content:
|
|
572
|
-
type: "
|
|
549
|
+
content: HARMONY_AGENTS_SECTION,
|
|
550
|
+
type: "markdown",
|
|
573
551
|
});
|
|
574
552
|
|
|
575
553
|
// Codex prompt file
|
|
@@ -582,7 +560,7 @@ arguments:
|
|
|
582
560
|
required: true
|
|
583
561
|
---
|
|
584
562
|
|
|
585
|
-
${
|
|
563
|
+
${renderWorkflowPrompt({ cardArgument: "{{card}}", agentIdentifier: "codex", agentName: "OpenAI Codex" })}
|
|
586
564
|
`;
|
|
587
565
|
|
|
588
566
|
if (installMode === "global") {
|
|
@@ -652,7 +630,7 @@ alwaysApply: false
|
|
|
652
630
|
|
|
653
631
|
When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
|
|
654
632
|
|
|
655
|
-
${
|
|
633
|
+
${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "cursor", agentName: "Cursor" })}
|
|
656
634
|
`;
|
|
657
635
|
|
|
658
636
|
if (installMode === "global") {
|
|
@@ -708,7 +686,7 @@ description: Activate when user asks to work on a Harmony card (references like
|
|
|
708
686
|
|
|
709
687
|
When working on a Harmony card:
|
|
710
688
|
|
|
711
|
-
${
|
|
689
|
+
${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "windsurf", agentName: "Windsurf" })}
|
|
712
690
|
`;
|
|
713
691
|
|
|
714
692
|
if (installMode === "global") {
|
|
@@ -1576,6 +1554,11 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1576
1554
|
` ${colors.success("\u2713")} ${colors.dim(formatPath(writtenLocalConfigPath, home))} ${colors.dim("(created)")}`,
|
|
1577
1555
|
);
|
|
1578
1556
|
|
|
1557
|
+
// The scan runs AFTER the global mirror below, deliberately: it spawns a
|
|
1558
|
+
// child with inherited stdio for as long as this repo's test suite takes,
|
|
1559
|
+
// and a Ctrl-C there must not cost the operator the global default that
|
|
1560
|
+
// every server started outside this directory reads (#893).
|
|
1561
|
+
//
|
|
1579
1562
|
// Mirror the choice into the GLOBAL default, explicitly (#893). The local
|
|
1580
1563
|
// file above already pins this directory; this second write is what gives a
|
|
1581
1564
|
// server started elsewhere — Claude Desktop, another repo — a context at
|
|
@@ -1592,6 +1575,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1592
1575
|
{ global: true },
|
|
1593
1576
|
);
|
|
1594
1577
|
}
|
|
1578
|
+
|
|
1579
|
+
await offerCommandScan(dirname(writtenLocalConfigPath), assumeYes);
|
|
1595
1580
|
}
|
|
1596
1581
|
|
|
1597
1582
|
// Step 10: Show completion message
|
|
@@ -1686,3 +1671,99 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1686
1671
|
}
|
|
1687
1672
|
console.log("");
|
|
1688
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* The command that proposes a repo's `commands` block by running it.
|
|
1677
|
+
*
|
|
1678
|
+
* `@latest` is this repo's own convention for an `npx` spec (`@gethmy/mcp@latest`
|
|
1679
|
+
* in every documented invocation) and it is load-bearing here: `scan-commands`
|
|
1680
|
+
* arrives in a release later than several already published, and a warm npx
|
|
1681
|
+
* cache of an older `@gethmy/agent` would otherwise hand the operator the
|
|
1682
|
+
* "may predate it" fallback forever with no way to see why.
|
|
1683
|
+
*/
|
|
1684
|
+
const SCAN_ARGV = ["--yes", "@gethmy/agent@latest", "scan-commands"] as const;
|
|
1685
|
+
|
|
1686
|
+
function scanTip(): void {
|
|
1687
|
+
console.log(
|
|
1688
|
+
` ${colors.dim("Tip: run")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)} ${colors.dim("to have the daemon prove which build, test and dev commands this repo has, and write them into the pin.")}`,
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Offer to scan this repo's commands, and run the scanner when asked (#1116).
|
|
1694
|
+
*
|
|
1695
|
+
* Card 1116's first acceptance criterion is that the scanner RUNS when a
|
|
1696
|
+
* project is set up — this is that moment, and a printed tip is not a run.
|
|
1697
|
+
* What this package must not do is scan by itself: the scan executes the
|
|
1698
|
+
* repo's own build, test and dev commands, and `@gethmy/mcp` neither depends
|
|
1699
|
+
* on `@gethmy/harness` (which owns both the execution and the schema) nor
|
|
1700
|
+
* spawns a toolchain anywhere else. So it does what the tip asked a person to
|
|
1701
|
+
* do — spawn the agent CLI — which keeps the dependency direction intact
|
|
1702
|
+
* (agent → harness → shared, mcp beside them) and re-types no schema.
|
|
1703
|
+
*
|
|
1704
|
+
* Three properties, each deliberate.
|
|
1705
|
+
*
|
|
1706
|
+
* **Asked, never assumed.** The scan starts this repo's build, test and dev
|
|
1707
|
+
* commands, which is minutes of work and arbitrary code from the checkout. A
|
|
1708
|
+
* person is present at setup and is the right one to say yes.
|
|
1709
|
+
*
|
|
1710
|
+
* **Non-interactive setups are not scanned at all** — `--yes`, a pipe, a
|
|
1711
|
+
* coding agent — even though `--yes` means yes to everything else. Those two
|
|
1712
|
+
* are not the same question: the other confirmations write files this command
|
|
1713
|
+
* already owns, and this one hands control to another package's process for
|
|
1714
|
+
* an unbounded time. They get the tip, i.e. exactly the behaviour that
|
|
1715
|
+
* shipped before this.
|
|
1716
|
+
*
|
|
1717
|
+
* **A failure is not an error.** The published `@gethmy/agent` gains this
|
|
1718
|
+
* subcommand only on its next release, and `npx` needs a network. Either way
|
|
1719
|
+
* the fallback is the tip, so an older agent degrades to the previous
|
|
1720
|
+
* behaviour rather than turning setup red over an optional convenience.
|
|
1721
|
+
*
|
|
1722
|
+
* It deliberately does NOT pass `--write`: the proposal is printed for a
|
|
1723
|
+
* person to read, which is card 1116's fourth criterion (never applied
|
|
1724
|
+
* silently), and `scan-commands --write` is the separate, explicit act.
|
|
1725
|
+
*/
|
|
1726
|
+
export async function offerCommandScan(
|
|
1727
|
+
repoDir: string,
|
|
1728
|
+
assumeYes: boolean,
|
|
1729
|
+
): Promise<void> {
|
|
1730
|
+
if (assumeYes) {
|
|
1731
|
+
scanTip();
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const scan = await confirmOrDefault(assumeYes, {
|
|
1736
|
+
message:
|
|
1737
|
+
"Scan this repo's commands now? It runs your build, test and dev scripts once to prove which ones exist. Nothing is written.",
|
|
1738
|
+
initialValue: true,
|
|
1739
|
+
});
|
|
1740
|
+
// Ctrl-C ends setup, as it does at every other prompt in this file. A
|
|
1741
|
+
// cancel folded into "no" would carry on through the remaining steps as if
|
|
1742
|
+
// the person had answered, which is the one reading they did not give.
|
|
1743
|
+
if (p.isCancel(scan)) {
|
|
1744
|
+
p.cancel("Setup cancelled.");
|
|
1745
|
+
process.exit(0);
|
|
1746
|
+
}
|
|
1747
|
+
if (!scan) {
|
|
1748
|
+
scanTip();
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
console.log(
|
|
1753
|
+
` ${colors.dim("Running")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)}${colors.dim(" …")}`,
|
|
1754
|
+
);
|
|
1755
|
+
const result = spawnSync("npx", [...SCAN_ARGV], {
|
|
1756
|
+
cwd: repoDir,
|
|
1757
|
+
stdio: "inherit",
|
|
1758
|
+
});
|
|
1759
|
+
if (result.error || result.status !== 0) {
|
|
1760
|
+
console.log(
|
|
1761
|
+
` ${colors.dim("The scan did not run — your installed @gethmy/agent may predate it.")}`,
|
|
1762
|
+
);
|
|
1763
|
+
scanTip();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
console.log(
|
|
1767
|
+
` ${colors.dim("Re-run it with")} ${colors.highlight("--write")} ${colors.dim("to merge that block into the pin.")}`,
|
|
1768
|
+
);
|
|
1769
|
+
}
|
package/src/tui/writer.ts
CHANGED
|
@@ -204,6 +204,112 @@ export function appendToToml(
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Markers delimiting the Harmony-owned section of a shared markdown file.
|
|
209
|
+
* Everything between them is package-generated and refreshed on reinstall;
|
|
210
|
+
* everything outside them belongs to the project and is never rewritten.
|
|
211
|
+
*/
|
|
212
|
+
export const MARKDOWN_SECTION_START = "<!-- harmony:start -->";
|
|
213
|
+
export const MARKDOWN_SECTION_END = "<!-- harmony:end -->";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Locate the Harmony-owned section: the first WELL-FORMED marker pair — a
|
|
217
|
+
* START, the first END after it, and no second START in between.
|
|
218
|
+
*
|
|
219
|
+
* **This choice is the whole safety argument of `mergeMarkdownSection`,** and
|
|
220
|
+
* the two obvious rules are each unsafe in one direction. First-START-first-END
|
|
221
|
+
* swallows everything between a stray START above our section and our real END.
|
|
222
|
+
* Last-START-first-END-after does the mirror: a project quoting both markers
|
|
223
|
+
* BELOW our section loses whatever sits between those two mentions. A file that
|
|
224
|
+
* merely shows the markers inside a code fence hits one or the other.
|
|
225
|
+
*
|
|
226
|
+
* Requiring the span to hold no START defeats both, because a stray marker
|
|
227
|
+
* disqualifies the candidate it sits inside and the scan moves past it. What
|
|
228
|
+
* this does NOT do is notice a file holding two complete sections: it updates
|
|
229
|
+
* the first and leaves the second stale. That is a duplicate to spot by eye,
|
|
230
|
+
* not content destroyed, which is the trade this function exists to make.
|
|
231
|
+
*/
|
|
232
|
+
function findSection(text: string): { start: number; end: number } | null {
|
|
233
|
+
let from = 0;
|
|
234
|
+
while (true) {
|
|
235
|
+
const start = text.indexOf(MARKDOWN_SECTION_START, from);
|
|
236
|
+
if (start === -1) return null;
|
|
237
|
+
const bodyFrom = start + MARKDOWN_SECTION_START.length;
|
|
238
|
+
const end = text.indexOf(MARKDOWN_SECTION_END, bodyFrom);
|
|
239
|
+
if (end === -1) return null;
|
|
240
|
+
const nextStart = text.indexOf(MARKDOWN_SECTION_START, bodyFrom);
|
|
241
|
+
if (nextStart === -1 || nextStart > end) return { start, end };
|
|
242
|
+
// `start` is unpaired. `nextStart > from` always, so the scan terminates.
|
|
243
|
+
from = nextStart;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Merge a Harmony-owned section into a shared markdown file (AGENTS.md).
|
|
249
|
+
*
|
|
250
|
+
* `AGENTS.md` is the project's file, not ours. It may already carry the
|
|
251
|
+
* docs-step scaffold written earlier in the same run, or a file the user has
|
|
252
|
+
* maintained by hand for months. A plain `writeFile` destroyed both: setup runs
|
|
253
|
+
* with `force: true` on every fresh install (`needsSkills`), which bypasses the
|
|
254
|
+
* exists-skip, and the agent files are pushed onto `allFiles` AFTER the docs
|
|
255
|
+
* scaffold — so the last write of `AGENTS.md` won and the scaffold vanished in
|
|
256
|
+
* the same breath as the line announcing it. Hence markers and a merge (#1124).
|
|
257
|
+
*
|
|
258
|
+
* - File absent → create it holding just the section.
|
|
259
|
+
* - A section is present (see `findSection`) → replace what lies between its
|
|
260
|
+
* markers, and only under `force` (matching `appendToToml`) — the section is
|
|
261
|
+
* package-generated, so a reinstall is when it should be refreshed.
|
|
262
|
+
* - No section → append. The file's existing content is never rewritten.
|
|
263
|
+
*/
|
|
264
|
+
export function mergeMarkdownSection(
|
|
265
|
+
filePath: string,
|
|
266
|
+
content: string,
|
|
267
|
+
options: WriteOptions = {},
|
|
268
|
+
): FileResult {
|
|
269
|
+
const section = `${MARKDOWN_SECTION_START}\n${content.trim()}\n${MARKDOWN_SECTION_END}\n`;
|
|
270
|
+
|
|
271
|
+
if (!existsSync(filePath)) {
|
|
272
|
+
try {
|
|
273
|
+
ensureDir(dirname(filePath));
|
|
274
|
+
writeFileSync(filePath, section, { mode: 0o644 });
|
|
275
|
+
return { path: filePath, action: "create" };
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return {
|
|
278
|
+
path: filePath,
|
|
279
|
+
action: "skip",
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
287
|
+
const found = findSection(existing);
|
|
288
|
+
|
|
289
|
+
if (found) {
|
|
290
|
+
if (!options.force) return { path: filePath, action: "skip" };
|
|
291
|
+
const updated =
|
|
292
|
+
existing.slice(0, found.start) +
|
|
293
|
+
section.trimEnd() +
|
|
294
|
+
existing.slice(found.end + MARKDOWN_SECTION_END.length);
|
|
295
|
+
writeFileSync(filePath, updated, { mode: 0o644 });
|
|
296
|
+
return { path: filePath, action: "update" };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// No section we can identify — every byte of this file is the project's.
|
|
300
|
+
// Append, never overwrite, however forceful the caller is.
|
|
301
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
302
|
+
writeFileSync(filePath, existing + separator + section, { mode: 0o644 });
|
|
303
|
+
return { path: filePath, action: "merge" };
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
path: filePath,
|
|
307
|
+
action: "skip",
|
|
308
|
+
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
207
313
|
/**
|
|
208
314
|
* Write multiple files with progress display
|
|
209
315
|
*/
|
|
@@ -211,7 +317,7 @@ export async function writeFilesWithProgress(
|
|
|
211
317
|
files: Array<{
|
|
212
318
|
path: string;
|
|
213
319
|
content: string;
|
|
214
|
-
type: "text" | "json" | "toml";
|
|
320
|
+
type: "text" | "json" | "toml" | "markdown";
|
|
215
321
|
jsonKey?: string;
|
|
216
322
|
tomlSection?: string;
|
|
217
323
|
mode?: number;
|
|
@@ -232,6 +338,8 @@ export async function writeFilesWithProgress(
|
|
|
232
338
|
result = mergeJsonFile(file.path, jsonContent, options);
|
|
233
339
|
} else if (file.type === "toml" && file.tomlSection) {
|
|
234
340
|
result = appendToToml(file.path, file.tomlSection, file.content, options);
|
|
341
|
+
} else if (file.type === "markdown") {
|
|
342
|
+
result = mergeMarkdownSection(file.path, file.content, options);
|
|
235
343
|
} else {
|
|
236
344
|
result = writeFile(file.path, file.content, {
|
|
237
345
|
...options,
|
|
@@ -256,7 +364,15 @@ export async function writeFilesWithProgress(
|
|
|
256
364
|
} else if (result.action === "skip") {
|
|
257
365
|
console.log(messages.fileSkipped(displayPath));
|
|
258
366
|
} else {
|
|
259
|
-
|
|
367
|
+
// `update` used to fall through to "created" — harmless while it was rare,
|
|
368
|
+
// and a lie the moment AGENTS.md started taking the merge path on every
|
|
369
|
+
// reinstall, telling the user a months-old file was just created (#1124).
|
|
370
|
+
const actionLabel =
|
|
371
|
+
result.action === "create"
|
|
372
|
+
? "created"
|
|
373
|
+
: result.action === "merge"
|
|
374
|
+
? "merged"
|
|
375
|
+
: "updated";
|
|
260
376
|
console.log(
|
|
261
377
|
` ${colors.success("\u2713")} ${colors.dim(displayPath)} ${colors.dim(`(${actionLabel})`)}`,
|
|
262
378
|
);
|