@davesheffer/hunch 1.20.0-rc.6 → 1.20.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/README.md +3 -3
- package/dist/cli/index.js +30 -4
- package/dist/core/landscapeAdoption.js +87 -4
- package/dist/integrations/claudemd.js +1 -1
- package/dist/mcp/server.js +38 -13
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ repository, separate from the code repository. Hunch does not host it. Create a
|
|
|
171
171
|
that every teammate can access, install Hunch on team machines and CI, then have one maintainer run:
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
|
-
npm i -g @davesheffer/hunch@1.
|
|
174
|
+
npm i -g @davesheffer/hunch@1.20.1
|
|
175
175
|
hunch shared --repo git@github.com:acme/project-hunch-memory.git
|
|
176
176
|
git add .gitignore .hunch/team.json
|
|
177
177
|
git commit -m "chore: connect shared Hunch memory"
|
|
@@ -186,7 +186,7 @@ printed by Hunch. Omit `--migrate` for a new setup.
|
|
|
186
186
|
After the pointer commit lands, teammates need Hunch installed and Git access to the memory repo:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
npm i -g @davesheffer/hunch@1.
|
|
189
|
+
npm i -g @davesheffer/hunch@1.20.1
|
|
190
190
|
git pull
|
|
191
191
|
hunch init
|
|
192
192
|
hunch doctor
|
|
@@ -234,7 +234,7 @@ but stops automatic memory commits and pushes. As a team-coordinated rollback, r
|
|
|
234
234
|
commit to stop discovery after teammates pull the revert. Existing machines retain their ignored
|
|
235
235
|
local overlay until they are deliberately disconnected; do not delete the memory repo as part of a
|
|
236
236
|
rollback. For this rollout, reinstall the previous published package with
|
|
237
|
-
`npm i -g @davesheffer/hunch@1.
|
|
237
|
+
`npm i -g @davesheffer/hunch@1.20.0`; the release receipt resolves and records the verified rollback
|
|
238
238
|
target from the npm registry instead of trusting Git tags. Pause enforcement first as shown above,
|
|
239
239
|
and keep every team client on the same release before resuming Matrix policy workflows.
|
|
240
240
|
|
package/dist/cli/index.js
CHANGED
|
@@ -83,7 +83,8 @@ import { pendingEscalations, policyEscalations } from "../core/escalations.js";
|
|
|
83
83
|
import { premiseEscalations } from "../core/premises.js";
|
|
84
84
|
import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
|
|
85
85
|
import { compareCandidates } from "../core/compare.js";
|
|
86
|
-
import {
|
|
86
|
+
import { compareCodeUnits } from "../core/canonicalOrder.js";
|
|
87
|
+
import { MAX_LANDSCAPE_REFRESH_REVISIONS, planLandscapeAdoption, } from "../core/landscapeAdoption.js";
|
|
87
88
|
import { discoverRepositoryLandscape } from "../extractors/landscapeDiscovery.js";
|
|
88
89
|
import { checkConformance } from "../core/conformance.js";
|
|
89
90
|
import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
|
|
@@ -2911,22 +2912,46 @@ landscapeCmd
|
|
|
2911
2912
|
.option("--all", "adopt every candidate in the reviewed discovery")
|
|
2912
2913
|
.option("--candidate <hashes...>", "adopt only these candidate hashes")
|
|
2913
2914
|
.option("--acknowledge-issues", "confirm that the printed discovery issues were reviewed")
|
|
2915
|
+
.option("--refresh-reviewed", "replace only byte-proven records from an older reviewed exact revision")
|
|
2914
2916
|
.action((opts) => {
|
|
2915
2917
|
if (opts.all && opts.candidate?.length)
|
|
2916
2918
|
return fail("choose either --all or --candidate, not both");
|
|
2917
2919
|
if (!opts.all && !opts.candidate?.length)
|
|
2918
2920
|
return fail("choose --all or name reviewed hashes with --candidate <hashes...>");
|
|
2921
|
+
if (opts.refreshReviewed && !opts.all)
|
|
2922
|
+
return fail("--refresh-reviewed requires --all so the complete prior review can be proven");
|
|
2919
2923
|
const { store, root } = storeFor();
|
|
2920
2924
|
try {
|
|
2921
2925
|
const discovery = discoverRepositoryLandscape(root, opts.ref);
|
|
2926
|
+
const existingResources = store.recs("resources");
|
|
2927
|
+
const existingRelationships = store.recs("edges");
|
|
2928
|
+
const selectedHashes = new Set(opts.all
|
|
2929
|
+
? [...discovery.resources, ...discovery.relationships].map((candidate) => candidate.candidateHash)
|
|
2930
|
+
: opts.candidate);
|
|
2931
|
+
const selectedIds = new Set([...discovery.resources, ...discovery.relationships]
|
|
2932
|
+
.filter((candidate) => selectedHashes.has(candidate.candidateHash))
|
|
2933
|
+
.map((candidate) => candidate.record.id));
|
|
2934
|
+
const previousRevisions = opts.refreshReviewed
|
|
2935
|
+
? [...new Set([...existingResources, ...existingRelationships]
|
|
2936
|
+
.filter((record) => selectedIds.has(record.id))
|
|
2937
|
+
.map((record) => record.currentness?.source_revision)
|
|
2938
|
+
.filter((revision) => Boolean(revision) && revision !== discovery.sourceRevision))]
|
|
2939
|
+
.sort(compareCodeUnits)
|
|
2940
|
+
: [];
|
|
2941
|
+
if (previousRevisions.length > MAX_LANDSCAPE_REFRESH_REVISIONS) {
|
|
2942
|
+
return fail(`reviewed refresh needs ${previousRevisions.length} prior revisions; the safe limit is ${MAX_LANDSCAPE_REFRESH_REVISIONS}`);
|
|
2943
|
+
}
|
|
2944
|
+
const previousDiscoveries = previousRevisions.map((revision) => discoverRepositoryLandscape(root, revision));
|
|
2922
2945
|
const plan = planLandscapeAdoption({
|
|
2923
2946
|
discovery,
|
|
2924
2947
|
expectedDiscoveryHash: opts.expected,
|
|
2925
2948
|
reviewer: opts.reviewedBy,
|
|
2926
2949
|
candidateHashes: opts.all ? "all" : opts.candidate,
|
|
2927
2950
|
acknowledgeIssues: opts.acknowledgeIssues,
|
|
2928
|
-
existingResources
|
|
2929
|
-
existingRelationships
|
|
2951
|
+
existingResources,
|
|
2952
|
+
existingRelationships,
|
|
2953
|
+
refreshReviewed: opts.refreshReviewed,
|
|
2954
|
+
previousDiscoveries,
|
|
2930
2955
|
});
|
|
2931
2956
|
for (const resource of plan.resourcesToWrite)
|
|
2932
2957
|
store.putCapture("resources", resource);
|
|
@@ -2937,7 +2962,8 @@ landscapeCmd
|
|
|
2937
2962
|
pumpMemoryHome(store, root, store.captureHome(false), "hunch: adopt reviewed Engineering Landscape candidates");
|
|
2938
2963
|
}
|
|
2939
2964
|
console.log(JSON.stringify(plan.receipt, null, 2));
|
|
2940
|
-
|
|
2965
|
+
const refreshed = plan.refreshedResourceIds.length + plan.refreshedRelationshipIds.length;
|
|
2966
|
+
console.log(`✓ accepted ${plan.receipt.acceptedResourceIds.length} resource(s) and ${plan.receipt.acceptedRelationshipIds.length} relationship(s); wrote ${plan.resourcesToWrite.length + plan.relationshipsToWrite.length} (${refreshed} refreshed), reused ${plan.receipt.reusedResourceIds.length + plan.receipt.reusedRelationshipIds.length}.`);
|
|
2941
2967
|
}
|
|
2942
2968
|
catch (error) {
|
|
2943
2969
|
fail(error instanceof Error ? error.message : String(error));
|
|
@@ -5,6 +5,7 @@ export const LANDSCAPE_REVIEW_SCHEMA_VERSION = "hunch.landscape-review/1";
|
|
|
5
5
|
export const LANDSCAPE_ADOPTION_RECEIPT_SCHEMA_VERSION = "hunch.landscape-adoption-receipt/1";
|
|
6
6
|
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
7
7
|
const MAX_SELECTIONS = 4_096;
|
|
8
|
+
export const MAX_LANDSCAPE_REFRESH_REVISIONS = 16;
|
|
8
9
|
function sortedUnique(values, label) {
|
|
9
10
|
if (values.length > MAX_SELECTIONS)
|
|
10
11
|
throw new Error(`landscape ${label} exceeds the bounded selection limit`);
|
|
@@ -168,6 +169,71 @@ function sameAcceptedCandidate(record, candidate, discoveryHash) {
|
|
|
168
169
|
// when the entire reviewed record still equals the candidate-derived value.
|
|
169
170
|
return landscapeContentHash(record) === landscapeContentHash(expected);
|
|
170
171
|
}
|
|
172
|
+
function previousDiscoveriesForRefresh(input) {
|
|
173
|
+
const previous = input.previousDiscoveries ?? [];
|
|
174
|
+
if (!input.refreshReviewed) {
|
|
175
|
+
if (previous.length > 0)
|
|
176
|
+
throw new Error("landscape previous discoveries require explicit reviewed refresh authority");
|
|
177
|
+
return new Map();
|
|
178
|
+
}
|
|
179
|
+
if (input.candidateHashes !== "all") {
|
|
180
|
+
throw new Error("landscape reviewed refresh requires the complete candidate set (--all)");
|
|
181
|
+
}
|
|
182
|
+
if (previous.length > MAX_LANDSCAPE_REFRESH_REVISIONS) {
|
|
183
|
+
throw new Error(`landscape reviewed refresh exceeds the bounded prior-revision proof limit (${MAX_LANDSCAPE_REFRESH_REVISIONS})`);
|
|
184
|
+
}
|
|
185
|
+
const byRevision = new Map();
|
|
186
|
+
for (const discovery of previous) {
|
|
187
|
+
assertLandscapeDiscoveryIntegrity(discovery);
|
|
188
|
+
if (discovery.repositoryRootIdentity !== input.discovery.repositoryRootIdentity) {
|
|
189
|
+
throw new Error("landscape reviewed refresh prior discovery belongs to a different repository");
|
|
190
|
+
}
|
|
191
|
+
if (discovery.sourceRevision === input.discovery.sourceRevision) {
|
|
192
|
+
throw new Error("landscape reviewed refresh requires an older exact discovery revision");
|
|
193
|
+
}
|
|
194
|
+
if (byRevision.has(discovery.sourceRevision)) {
|
|
195
|
+
throw new Error(`landscape reviewed refresh contains duplicate prior revision: ${discovery.sourceRevision}`);
|
|
196
|
+
}
|
|
197
|
+
byRevision.set(discovery.sourceRevision, discovery);
|
|
198
|
+
}
|
|
199
|
+
return byRevision;
|
|
200
|
+
}
|
|
201
|
+
function isProvenPreviousAdoption(record, previousDiscoveries) {
|
|
202
|
+
const sourceRevision = record.currentness?.source_revision;
|
|
203
|
+
if (!sourceRevision)
|
|
204
|
+
return false;
|
|
205
|
+
const previous = previousDiscoveries.get(sourceRevision);
|
|
206
|
+
if (!previous)
|
|
207
|
+
return false;
|
|
208
|
+
const candidates = record.schema === "hunch.resource/1"
|
|
209
|
+
? previous.resources
|
|
210
|
+
: previous.relationships;
|
|
211
|
+
const candidate = candidates.find((value) => value.record.id === record.id);
|
|
212
|
+
if (!candidate || !sameAcceptedCandidate(record, candidate, previous.discoveryHash))
|
|
213
|
+
return false;
|
|
214
|
+
const reviewer = record.metadata.landscape_reviewed_by;
|
|
215
|
+
const reviewedAt = record.metadata.landscape_reviewed_at;
|
|
216
|
+
const reviewId = record.metadata.landscape_review_id;
|
|
217
|
+
if (typeof reviewer !== "string" || typeof reviewedAt !== "string" || typeof reviewId !== "string")
|
|
218
|
+
return false;
|
|
219
|
+
try {
|
|
220
|
+
const allCandidateHashes = [...previous.resources, ...previous.relationships]
|
|
221
|
+
.map((value) => value.candidateHash)
|
|
222
|
+
.sort(compareCodeUnits);
|
|
223
|
+
const reconstructed = reviewFor({
|
|
224
|
+
discovery: previous,
|
|
225
|
+
expectedDiscoveryHash: previous.discoveryHash,
|
|
226
|
+
reviewer,
|
|
227
|
+
reviewedAt,
|
|
228
|
+
candidateHashes: "all",
|
|
229
|
+
acknowledgeIssues: previous.issues.length > 0,
|
|
230
|
+
}, allCandidateHashes);
|
|
231
|
+
return reconstructed.reviewId === reviewId;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
171
237
|
/**
|
|
172
238
|
* Convert an exact candidate discovery into a prevalidated write plan.
|
|
173
239
|
*
|
|
@@ -208,6 +274,7 @@ export function planLandscapeAdoption(input) {
|
|
|
208
274
|
}
|
|
209
275
|
}
|
|
210
276
|
const review = reviewFor(input, selectedCandidateHashes);
|
|
277
|
+
const previousDiscoveries = previousDiscoveriesForRefresh(input);
|
|
211
278
|
const existingResources = new Map();
|
|
212
279
|
for (const value of input.existingResources ?? []) {
|
|
213
280
|
const record = ResourceSchema.parse(value);
|
|
@@ -226,13 +293,21 @@ export function planLandscapeAdoption(input) {
|
|
|
226
293
|
const relationshipsToWrite = [];
|
|
227
294
|
const reusedResourceIds = [];
|
|
228
295
|
const reusedRelationshipIds = [];
|
|
296
|
+
const refreshedResourceIds = [];
|
|
297
|
+
const refreshedRelationshipIds = [];
|
|
229
298
|
for (const candidate of selectedResources) {
|
|
230
299
|
const existing = existingResources.get(candidate.record.id);
|
|
231
300
|
if (existing) {
|
|
232
|
-
if (
|
|
301
|
+
if (sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
|
|
302
|
+
reusedResourceIds.push(existing.id);
|
|
303
|
+
}
|
|
304
|
+
else if (input.refreshReviewed && isProvenPreviousAdoption(existing, previousDiscoveries)) {
|
|
305
|
+
resourcesToWrite.push(acceptedResource(candidate, input.discovery.discoveryHash, review));
|
|
306
|
+
refreshedResourceIds.push(existing.id);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
233
309
|
throw new Error(`landscape resource ${candidate.record.id} already exists with different reviewed content`);
|
|
234
310
|
}
|
|
235
|
-
reusedResourceIds.push(existing.id);
|
|
236
311
|
}
|
|
237
312
|
else {
|
|
238
313
|
resourcesToWrite.push(acceptedResource(candidate, input.discovery.discoveryHash, review));
|
|
@@ -241,10 +316,16 @@ export function planLandscapeAdoption(input) {
|
|
|
241
316
|
for (const candidate of selectedRelationships) {
|
|
242
317
|
const existing = existingRelationships.get(candidate.record.id);
|
|
243
318
|
if (existing) {
|
|
244
|
-
if (
|
|
319
|
+
if (sameAcceptedCandidate(existing, candidate, input.discovery.discoveryHash)) {
|
|
320
|
+
reusedRelationshipIds.push(existing.id);
|
|
321
|
+
}
|
|
322
|
+
else if (input.refreshReviewed && isProvenPreviousAdoption(existing, previousDiscoveries)) {
|
|
323
|
+
relationshipsToWrite.push(acceptedRelationship(candidate, input.discovery.discoveryHash, review));
|
|
324
|
+
refreshedRelationshipIds.push(existing.id);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
245
327
|
throw new Error(`landscape relationship ${candidate.record.id} already exists with different reviewed content`);
|
|
246
328
|
}
|
|
247
|
-
reusedRelationshipIds.push(existing.id);
|
|
248
329
|
}
|
|
249
330
|
else {
|
|
250
331
|
relationshipsToWrite.push(acceptedRelationship(candidate, input.discovery.discoveryHash, review));
|
|
@@ -266,6 +347,8 @@ export function planLandscapeAdoption(input) {
|
|
|
266
347
|
receipt: { ...receiptUnsigned, receiptId },
|
|
267
348
|
resourcesToWrite,
|
|
268
349
|
relationshipsToWrite,
|
|
350
|
+
refreshedResourceIds: refreshedResourceIds.sort(compareCodeUnits),
|
|
351
|
+
refreshedRelationshipIds: refreshedRelationshipIds.sort(compareCodeUnits),
|
|
269
352
|
};
|
|
270
353
|
}
|
|
271
354
|
//# sourceMappingURL=landscapeAdoption.js.map
|
|
@@ -46,7 +46,7 @@ export function renderHunchSection(store, root) {
|
|
|
46
46
|
lines.push("**Consult Hunch via the `hunch_*` MCP tools — pick by MOMENT, not from memory:**");
|
|
47
47
|
lines.push("");
|
|
48
48
|
lines.push("**Orient (session/task start):**");
|
|
49
|
-
lines.push("- `hunch_context(
|
|
49
|
+
lines.push("- `hunch_context(target)` — the minimal relevant slice for what you're about to do; a task phrase falls back to the closest graph matches. **Call FIRST.**");
|
|
50
50
|
lines.push("- `hunch_structure(target?)` — the indexed shape of the repo/dir/file/symbol — orient from the graph, not grep rounds.");
|
|
51
51
|
lines.push("- `hunch_runbook(task)` — the proven steps for a recurring task, before re-deriving them.");
|
|
52
52
|
lines.push("- `hunch_escalations()` — the decisions only the HUMAN can make (including one exact imported ADR at a time, topic conflicts, and policy calls). Normally empty; when it isn't, ASK the user inline — an entry is a question, silence is never approval. Apply an ADR answer only through `hunch_review_imported_adr` with its printed source and review hashes.");
|
package/dist/mcp/server.js
CHANGED
|
@@ -477,14 +477,22 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
477
477
|
};
|
|
478
478
|
const pullTeamMemory = (force = false) => {
|
|
479
479
|
if (!store.privateDir)
|
|
480
|
-
return;
|
|
480
|
+
return "not_shared";
|
|
481
481
|
const now = Date.now();
|
|
482
482
|
if (!force && now < nextRemotePullAt)
|
|
483
|
-
return;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
483
|
+
return "cooldown";
|
|
484
|
+
let status;
|
|
485
|
+
try {
|
|
486
|
+
status = pullHunchStatus(store.privateDir, {
|
|
487
|
+
timeoutMs: 5_000,
|
|
488
|
+
remote: startupTeamRoute ?? advertisedTeamRemoteContract(root, join(store.privateDir, "..")),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
status = "failed";
|
|
493
|
+
}
|
|
494
|
+
notePull(status, Date.now());
|
|
495
|
+
return status;
|
|
488
496
|
};
|
|
489
497
|
// A source stamp is acknowledged ONLY after a stable, successful rebuild. If
|
|
490
498
|
// another process changes the atomic JSON tree during the rebuild, retry once;
|
|
@@ -626,10 +634,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
626
634
|
return err("The committed team memory destination is invalid or no longer matches this process. Refusing the stale graph; reconnect Hunch first.");
|
|
627
635
|
}
|
|
628
636
|
if (store.mode === "shared" && store.privateDir) {
|
|
629
|
-
|
|
630
|
-
pullTeamMemory();
|
|
631
|
-
}
|
|
632
|
-
catch { /* offline / lock held / invalid remote — use local */ }
|
|
637
|
+
pullTeamMemory();
|
|
633
638
|
// Recompute the full semantic + physical snapshot after the synchronous
|
|
634
639
|
// network seam. A paired team.json/origin change can occur while fetch is
|
|
635
640
|
// blocked; serving after that race would attach the old checkout to a new
|
|
@@ -1087,11 +1092,31 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
1087
1092
|
// -- hunch_current_decision (decision-grounding: current(topic)) ----------
|
|
1088
1093
|
server.registerTool("hunch_current_decision", {
|
|
1089
1094
|
title: "Current decision for a topic",
|
|
1090
|
-
description: "Decision-grounding: return the single CURRENT (accepted, non-superseded) decision anchored to a topic — the authoritative answer a doc or diff is checked against, plus what it rejected.
|
|
1095
|
+
description: "Decision-grounding: return the single CURRENT (accepted, non-superseded) decision anchored to a topic — the authoritative answer a doc or diff is checked against, plus what it rejected. A shared-store miss is confirmed against fresh team memory before Hunch says the topic has no current decision. If freshness is unavailable, or a topic has an unresolved collision (>1 live), it injects nothing (fail-safe).",
|
|
1091
1096
|
inputSchema: { topic: z.string().describe("the decision anchor, e.g. 'auth-transport'") },
|
|
1092
1097
|
}, async ({ topic }) => {
|
|
1093
|
-
|
|
1094
|
-
|
|
1098
|
+
let decs = store.recs("decisions");
|
|
1099
|
+
let live = liveForTopic(decs, topic);
|
|
1100
|
+
// A prior transient fetch failure may have placed ordinary tool traffic in
|
|
1101
|
+
// backoff. That is acceptable for cached positive reads, but an exact topic
|
|
1102
|
+
// miss is an authority claim: local absence must never be presented as team
|
|
1103
|
+
// absence. Bypass the cooldown once, then reread only after a successful
|
|
1104
|
+
// bounded convergence. An unavailable remote produces an explicit abstention.
|
|
1105
|
+
if (live.length === 0 && store.mode === "shared" && store.privateDir) {
|
|
1106
|
+
const status = pullTeamMemory(true);
|
|
1107
|
+
if (status !== "updated" && status !== "current") {
|
|
1108
|
+
return err(`Shared team memory refresh is ${status}; Hunch cannot confirm that topic "${topic}" has no current decision. Retry when the shared store is available.`);
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
if (store.sourceStamp() !== indexedSourceStamp)
|
|
1112
|
+
refreshIndex();
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return err(`Shared team memory refreshed, but its derived index could not be rebuilt; Hunch cannot confirm that topic "${topic}" is absent. Retry this read.`);
|
|
1116
|
+
}
|
|
1117
|
+
decs = store.recs("decisions");
|
|
1118
|
+
live = liveForTopic(decs, topic);
|
|
1119
|
+
}
|
|
1095
1120
|
if (live.length === 0)
|
|
1096
1121
|
return ok(`No current decision for topic "${topic}". (Un-anchored, or never captured.)`);
|
|
1097
1122
|
if (live.length > 1) {
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://hunch-pi.vercel.app",
|
|
10
|
-
"version": "1.20.
|
|
10
|
+
"version": "1.20.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.20.
|
|
16
|
+
"version": "1.20.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|