@davesheffer/hunch 1.43.0 → 1.44.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 +10 -0
- package/dist/cli/index.js +114 -19
- package/dist/core/changeProof.js +2 -1
- package/dist/core/commitrepair.d.ts +9 -3
- package/dist/core/commitrepair.js +23 -6
- package/dist/core/correction.d.ts +1 -1
- package/dist/core/correction.js +4 -3
- package/dist/core/docanchors.d.ts +11 -2
- package/dist/core/docanchors.js +232 -65
- package/dist/core/drift.d.ts +1 -1
- package/dist/core/drift.js +10 -3
- package/dist/core/eol.d.ts +2 -0
- package/dist/core/eol.js +6 -0
- package/dist/core/procstart.d.ts +15 -4
- package/dist/core/procstart.js +56 -18
- package/dist/core/runbookDraft.d.ts +7 -0
- package/dist/core/runbookDraft.js +43 -0
- package/dist/core/taskTouched.d.ts +2 -0
- package/dist/core/taskTouched.js +1 -1
- package/dist/extractors/git.d.ts +36 -6
- package/dist/extractors/git.js +86 -9
- package/dist/extractors/indexer.js +53 -14
- package/dist/extractors/k8sManifest.d.ts +31 -1
- package/dist/extractors/k8sManifest.js +133 -8
- package/dist/integrations/claudemd.d.ts +2 -0
- package/dist/integrations/claudemd.js +4 -2
- package/dist/integrations/gitignore.d.ts +7 -0
- package/dist/integrations/gitignore.js +18 -0
- package/dist/integrations/hooks.d.ts +15 -0
- package/dist/integrations/hooks.js +49 -1
- package/dist/integrations/mergeDriver.d.ts +2 -0
- package/dist/integrations/mergeDriver.js +2 -2
- package/dist/integrations/providers.d.ts +7 -0
- package/dist/integrations/providers.js +4 -4
- package/dist/integrations/scaffold.d.ts +5 -0
- package/dist/integrations/scaffold.js +12 -7
- package/dist/integrations/uninstall.d.ts +40 -0
- package/dist/integrations/uninstall.js +435 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +58 -4
- package/dist/serve/writelock.d.ts +7 -0
- package/dist/serve/writelock.js +30 -3
- package/dist/store/changeLedger.d.ts +5 -2
- package/dist/store/changeLedger.js +88 -37
- package/dist/store/hunchStore.js +4 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -144,6 +144,21 @@ export declare function hookStatus(root: string): {
|
|
|
144
144
|
postMerge: boolean;
|
|
145
145
|
postCheckout: boolean;
|
|
146
146
|
};
|
|
147
|
+
/** One git hook file `hunch uninstall` would change. `next === null` deletes the
|
|
148
|
+
* file (only a shebang was left); a `reason` means nothing is edited and the
|
|
149
|
+
* user removes the block by hand. */
|
|
150
|
+
export interface HookRemoval {
|
|
151
|
+
path: string;
|
|
152
|
+
hookName: string;
|
|
153
|
+
next?: string | null;
|
|
154
|
+
reason?: string;
|
|
155
|
+
}
|
|
156
|
+
/** Plan the removal of every Hunch-managed block from the hooks git runs (#449).
|
|
157
|
+
* Read-only. Only a hook file Hunch itself writes (manager "none", or an
|
|
158
|
+
* exec/exit file whose block it appended) is edited, and only a block with both
|
|
159
|
+
* markers; a hook manager's file, a tracked hooks dir, or an unclosed block is
|
|
160
|
+
* reported instead — the same lines Hunch refuses to write on install. */
|
|
161
|
+
export declare function planHookRemoval(root: string): HookRemoval[];
|
|
147
162
|
/** Doctor lines for where each installed/stale hook points (issue #315): hooks
|
|
148
163
|
* sharing one invocation share one line, and a line whose invocation is not the
|
|
149
164
|
* Hunch currently running says so. Informational — a hook may legitimately
|
|
@@ -20,7 +20,7 @@ import { homedir } from "node:os";
|
|
|
20
20
|
import { hooksDir, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
21
21
|
import { initiatorChildEnv } from "../synthesis/initiator.js";
|
|
22
22
|
import { HUNCH_NPX_PACKAGE_SPEC, HUNCH_PACKAGE_NAME } from "../core/version.js";
|
|
23
|
-
import { normalizeEol } from "../core/eol.js";
|
|
23
|
+
import { eolLengthAt, normalizeEol } from "../core/eol.js";
|
|
24
24
|
const MARK = "# >>> hunch post-commit >>>";
|
|
25
25
|
const ENDMARK = "# <<< hunch post-commit <<<";
|
|
26
26
|
const GIT_CONTEXT = { checkoutType: "$3", portable: false };
|
|
@@ -942,6 +942,54 @@ export function hookStatus(root) {
|
|
|
942
942
|
postCheckout: r.postCheckout.state === "installed",
|
|
943
943
|
};
|
|
944
944
|
}
|
|
945
|
+
/** Plan the removal of every Hunch-managed block from the hooks git runs (#449).
|
|
946
|
+
* Read-only. Only a hook file Hunch itself writes (manager "none", or an
|
|
947
|
+
* exec/exit file whose block it appended) is edited, and only a block with both
|
|
948
|
+
* markers; a hook manager's file, a tracked hooks dir, or an unclosed block is
|
|
949
|
+
* reported instead — the same lines Hunch refuses to write on install. */
|
|
950
|
+
export function planHookRemoval(root) {
|
|
951
|
+
const ends = {
|
|
952
|
+
[MARK]: ENDMARK, [PRE_MARK]: PRE_END, [GROUNDING_MERGE_MARK]: GROUNDING_MERGE_END,
|
|
953
|
+
[REPAIR_MERGE_MARK]: REPAIR_MERGE_END, [CHECKOUT_MARK]: CHECKOUT_END,
|
|
954
|
+
};
|
|
955
|
+
const byHook = new Map();
|
|
956
|
+
for (const [hookName, mark] of managedBlocks())
|
|
957
|
+
byHook.set(hookName, [...(byHook.get(hookName) ?? []), mark]);
|
|
958
|
+
const out = [];
|
|
959
|
+
for (const [hookName, marks] of byHook) {
|
|
960
|
+
const t = resolveHookTarget(root, hookName, marks[0]);
|
|
961
|
+
if (t.manager !== "none" && t.manager !== "exec-exit") {
|
|
962
|
+
const owner = readText(t.ownerFile);
|
|
963
|
+
if (owner !== null && marks.some((m) => owner.includes(m))) {
|
|
964
|
+
out.push({ path: t.ownerFile, hookName, reason: `${t.manager} owns this hook; remove the Hunch block from it by hand` });
|
|
965
|
+
}
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const cur = readText(t.hookPath);
|
|
969
|
+
if (cur === null || !marks.some((m) => cur.includes(m)))
|
|
970
|
+
continue;
|
|
971
|
+
let next = cur;
|
|
972
|
+
let damaged = false;
|
|
973
|
+
for (const mark of marks) {
|
|
974
|
+
if (!next.includes(mark))
|
|
975
|
+
continue;
|
|
976
|
+
const m = new RegExp(`${escapeRe(mark)}[\\s\\S]*?${escapeRe(ends[mark])}`).exec(next);
|
|
977
|
+
if (!m) {
|
|
978
|
+
damaged = true;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const end = m.index + m[0].length;
|
|
982
|
+
next = next.slice(0, m.index) + next.slice(end + eolLengthAt(next, end));
|
|
983
|
+
}
|
|
984
|
+
if (damaged) {
|
|
985
|
+
out.push({ path: t.hookPath, hookName, reason: "a Hunch block here has no closing marker; remove it by hand" });
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const body = normalizeEol(next).split("\n").filter((l, i) => !(i === 0 && l.startsWith("#!")));
|
|
989
|
+
out.push({ path: t.hookPath, hookName, next: body.every((l) => l.trim() === "") ? null : next });
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
945
993
|
/** Doctor lines for where each installed/stale hook points (issue #315): hooks
|
|
946
994
|
* sharing one invocation share one line, and a line whose invocation is not the
|
|
947
995
|
* Hunch currently running says so. Informational — a hook may legitimately
|
|
@@ -20,12 +20,12 @@ import { GROUNDING_DOC_PATHS } from "./providers.js";
|
|
|
20
20
|
//
|
|
21
21
|
// The five generated grounding docs get the OTHER driver — narrower in scope
|
|
22
22
|
// (it only ever touches a hard conflict confined to the counts sentence).
|
|
23
|
-
const ATTR_LINES = [
|
|
23
|
+
export const ATTR_LINES = [
|
|
24
24
|
".hunch/**/*.json merge=hunch",
|
|
25
25
|
".hunch/manifest.json merge=text",
|
|
26
26
|
...GROUNDING_DOC_PATHS.map((f) => `${f} merge=hunch-grounding`),
|
|
27
27
|
];
|
|
28
|
-
function targetRepositoryEnv() {
|
|
28
|
+
export function targetRepositoryEnv() {
|
|
29
29
|
const env = { ...process.env };
|
|
30
30
|
for (const key of [
|
|
31
31
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT",
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { HunchStore } from "../store/hunchStore.js";
|
|
2
2
|
import type { Invocation } from "./scaffold.js";
|
|
3
|
+
/** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
|
|
4
|
+
* non-empty file we cannot parse THROWS — overwriting it would silently wipe the
|
|
5
|
+
* user's other MCP servers. */
|
|
6
|
+
export declare function readJsonObj(file: string): Record<string, unknown>;
|
|
7
|
+
export declare function isHunchProviderHook(entry: unknown): boolean;
|
|
3
8
|
/** Cursor: .cursor/mcp.json — same `mcpServers` shape as Claude Desktop/Code. */
|
|
4
9
|
export declare function writeCursorMcp(root: string, inv: Invocation): string;
|
|
5
10
|
/** VS Code (Copilot agent mode): .vscode/mcp.json — root key is `servers`, and
|
|
@@ -20,6 +25,8 @@ export declare function writeAntigravityMcp(inv: Invocation, home?: string): str
|
|
|
20
25
|
* committed with the repository, so every clone gets the same private/local
|
|
21
26
|
* Hunch server without touching a user's home directory. */
|
|
22
27
|
export declare function writeAntigravityWorkspaceMcp(root: string, inv: Invocation): string;
|
|
28
|
+
export declare const TOML_START = "# >>> hunch mcp (managed) >>>";
|
|
29
|
+
export declare const TOML_END = "# <<< hunch mcp <<<";
|
|
23
30
|
/** Codex CLI: .codex/config.toml — `[mcp_servers.hunch]` stdio entry. We own only
|
|
24
31
|
* a marker-delimited block; any other TOML the user has is preserved. Paths use
|
|
25
32
|
* TOML single-quote LITERAL strings so Windows backslashes need no escaping. */
|
|
@@ -50,7 +50,7 @@ function hasJsoncComments(raw) {
|
|
|
50
50
|
/** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
|
|
51
51
|
* non-empty file we cannot parse THROWS — overwriting it would silently wipe the
|
|
52
52
|
* user's other MCP servers. */
|
|
53
|
-
function readJsonObj(file) {
|
|
53
|
+
export function readJsonObj(file) {
|
|
54
54
|
if (!existsSync(file))
|
|
55
55
|
return {};
|
|
56
56
|
const raw = readFileSync(file, "utf8");
|
|
@@ -128,7 +128,7 @@ function shellToken(part) {
|
|
|
128
128
|
function hookCommand(inv, provider) {
|
|
129
129
|
return [inv.command, ...inv.args, "hook", "--provider", provider].map(shellToken).join(" ");
|
|
130
130
|
}
|
|
131
|
-
function isHunchProviderHook(entry) {
|
|
131
|
+
export function isHunchProviderHook(entry) {
|
|
132
132
|
const e = entry && typeof entry === "object" ? entry : null;
|
|
133
133
|
// Codex (like Claude Code) nests commands under a matcher entry: an entry
|
|
134
134
|
// whose every nested hook is ours is ours; a mixed entry stays foreign.
|
|
@@ -210,8 +210,8 @@ export function writeAntigravityWorkspaceMcp(root, inv) {
|
|
|
210
210
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
211
211
|
return writeJson(file, json);
|
|
212
212
|
}
|
|
213
|
-
const TOML_START = "# >>> hunch mcp (managed) >>>";
|
|
214
|
-
const TOML_END = "# <<< hunch mcp <<<";
|
|
213
|
+
export const TOML_START = "# >>> hunch mcp (managed) >>>";
|
|
214
|
+
export const TOML_END = "# <<< hunch mcp <<<";
|
|
215
215
|
/** Codex CLI: .codex/config.toml — `[mcp_servers.hunch]` stdio entry. We own only
|
|
216
216
|
* a marker-delimited block; any other TOML the user has is preserved. Paths use
|
|
217
217
|
* TOML single-quote LITERAL strings so Windows backslashes need no escaping. */
|
|
@@ -23,6 +23,11 @@ export interface ClaudeHookInstall {
|
|
|
23
23
|
* non-empty file we cannot parse THROWS rather than clobbering the user's config.
|
|
24
24
|
*/
|
|
25
25
|
export declare function installClaudeHooks(root: string, hookCmd: string): ClaudeHookInstall;
|
|
26
|
+
/** Ownership marker for generated slash commands: its presence means Hunch may
|
|
27
|
+
* refresh the file; deleting the line hands the file to the user for good. */
|
|
28
|
+
export declare const CMD_MARKER = "<!-- hunch:generated \u2014 refreshed by hunch init; delete this line to take ownership -->";
|
|
29
|
+
/** File names `writeSlashCommands` writes under .claude/commands/. */
|
|
30
|
+
export declare const SLASH_COMMAND_FILES: readonly string[];
|
|
26
31
|
export declare function writeSlashCommands(root: string): {
|
|
27
32
|
written: string[];
|
|
28
33
|
skipped: string[];
|
|
@@ -233,13 +233,9 @@ export function installClaudeHooks(root, hookCmd) {
|
|
|
233
233
|
}
|
|
234
234
|
/** Ownership marker for generated slash commands: its presence means Hunch may
|
|
235
235
|
* refresh the file; deleting the line hands the file to the user for good. */
|
|
236
|
-
const CMD_MARKER = "<!-- hunch:generated — refreshed by hunch init; delete this line to take ownership -->";
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
mkdirSync(dir, { recursive: true });
|
|
240
|
-
const written = [];
|
|
241
|
-
const skipped = [];
|
|
242
|
-
const files = [
|
|
236
|
+
export const CMD_MARKER = "<!-- hunch:generated — refreshed by hunch init; delete this line to take ownership -->";
|
|
237
|
+
function slashCommandFiles() {
|
|
238
|
+
return [
|
|
243
239
|
["hunch-why.md", WHY_CMD],
|
|
244
240
|
["hunch-fix.md", FIX_CMD],
|
|
245
241
|
["hunch-fragile.md", FRAGILE_CMD],
|
|
@@ -248,6 +244,15 @@ export function writeSlashCommands(root) {
|
|
|
248
244
|
["audit.md", AUDIT_CMD],
|
|
249
245
|
["worktrees.md", WORKTREES_CMD],
|
|
250
246
|
];
|
|
247
|
+
}
|
|
248
|
+
/** File names `writeSlashCommands` writes under .claude/commands/. */
|
|
249
|
+
export const SLASH_COMMAND_FILES = Object.freeze(slashCommandFiles().map(([name]) => name));
|
|
250
|
+
export function writeSlashCommands(root) {
|
|
251
|
+
const dir = join(root, ".claude", "commands");
|
|
252
|
+
mkdirSync(dir, { recursive: true });
|
|
253
|
+
const written = [];
|
|
254
|
+
const skipped = [];
|
|
255
|
+
const files = slashCommandFiles();
|
|
251
256
|
for (const [name, body] of files) {
|
|
252
257
|
const p = join(dir, name);
|
|
253
258
|
// Generic names (capture/heal/audit) are plausibly the USER'S OWN commands;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The opt-out marker, at the repository root. Commit it so every clone stays opted out. */
|
|
2
|
+
export declare const OPT_OUT_FILE = ".hunch-removed";
|
|
3
|
+
export declare function isOptedOut(root: string): boolean;
|
|
4
|
+
export declare function optOutMessage(): string;
|
|
5
|
+
export interface UninstallStep {
|
|
6
|
+
/** Repo-relative (POSIX) where possible, else absolute. */
|
|
7
|
+
path: string;
|
|
8
|
+
kind: "edit" | "delete" | "create" | "keep";
|
|
9
|
+
note: string;
|
|
10
|
+
run?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface UninstallPlan {
|
|
13
|
+
root: string;
|
|
14
|
+
purge: boolean;
|
|
15
|
+
steps: UninstallStep[];
|
|
16
|
+
refused: Array<{
|
|
17
|
+
path: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/** Read-only: everything `hunch uninstall` would do in `root`. */
|
|
22
|
+
export declare function planUninstall(root: string, opts?: {
|
|
23
|
+
purge?: boolean;
|
|
24
|
+
}): UninstallPlan;
|
|
25
|
+
/** Apply a plan with no refusals. The opt-out marker goes first, so an interrupted
|
|
26
|
+
* run is already opted out. A step that fails at write time (a file held open by a
|
|
27
|
+
* running `hunch mcp`/`hunch serve` on Windows, a permission) does not stop the
|
|
28
|
+
* others; the failures are returned so the caller can say what is left and that a
|
|
29
|
+
* re-run — idempotent — finishes the job. */
|
|
30
|
+
/** What applyUninstall actually did: the steps that ran (so output never
|
|
31
|
+
* claims a step that was skipped), and the ones that failed. */
|
|
32
|
+
export interface UninstallResult {
|
|
33
|
+
applied: UninstallStep[];
|
|
34
|
+
failed: Array<{
|
|
35
|
+
path: string;
|
|
36
|
+
error: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export declare function applyUninstall(plan: UninstallPlan): UninstallResult;
|
|
40
|
+
export declare function formatUninstallPlan(plan: UninstallPlan, dryRun: boolean): string[];
|