heyspex 0.2.0 → 0.2.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/.claude-plugin/marketplace.json +14 -0
- package/bin/hs.mjs +163 -11
- package/dist/cli/hs-check.mjs +88 -46
- package/dist/cli/hs-claim.mjs +84 -39
- package/dist/cli/hs-connect.mjs +583 -129
- package/dist/cli/hs-defer.mjs +251 -30
- package/dist/cli/hs-dev.mjs +20 -8
- package/dist/cli/hs-env.mjs +62 -34
- package/dist/cli/hs-index-rebuild.mjs +288 -83
- package/dist/cli/hs-index-reconcile.mjs +397 -111
- package/dist/cli/hs-init.mjs +2143 -91
- package/dist/cli/hs-install.mjs +628 -106
- package/dist/cli/hs-join.mjs +686 -236
- package/dist/cli/hs-vault-pull.mjs +3189 -179
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +396 -178
- package/dist/cli/hs-workspace.mjs +289 -151
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
- package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/package.json +20 -2
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/run-hook.mjs +20 -2
- package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
- package/plugins/heyspex/hooks/hooks.json +94 -37
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
- package/scripts/claude-hooks/_run.mjs +316 -23
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
- package/scripts/claude-hooks/session-end.mjs +2 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
- package/scripts/cursor-hooks/manifest.mjs +46 -0
- package/scripts/heyspex-mode.mjs +20 -0
- package/scripts/hs-mode.mjs +1 -1
- package/scripts/hs-view.mjs +180 -17
- package/scripts/install-claude-code-hooks.mjs +3 -1
- package/scripts/lib/console-url-resolver.mjs +3 -2
- package/scripts/lib/customer-kit.mjs +327 -10
- package/scripts/lib/dev-mode.mjs +7 -6
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-log.mjs +2 -1
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -356
- package/scripts/lib/window-placement.mjs +109 -0
- package/spexharness/src/adapters/claude-code.mjs +11 -3
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/intent-classifier.mjs +4 -2
- package/spexharness/src/intent-directive.mjs +626 -541
- package/spexharness/src/rule-table.mjs +31 -1
- package/vendor/heyspex-mcp/account-switch.mjs +6 -5
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
- package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
- package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
- package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
- package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
- package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
- package/vendor/heyspex-mcp/cli.mjs +7 -1
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
- package/vendor/heyspex-mcp/console-transport.mjs +7 -1
- package/vendor/heyspex-mcp/credentials.mjs +9 -8
- package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
- package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
- package/vendor/heyspex-mcp/http-transport.mjs +2 -0
- package/vendor/heyspex-mcp/list-tools.mjs +1 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/plan-push.mjs +3 -2
- package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
- package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
- package/vendor/heyspex-mcp/promote-research.mjs +6 -0
- package/vendor/heyspex-mcp/read-path.mjs +162 -6
- package/vendor/heyspex-mcp/request-context.mjs +25 -1
- package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/session-owner.mjs +3 -2
- package/vendor/heyspex-mcp/session-tools.mjs +10 -0
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
- package/vendor/heyspex-mcp/split-research.mjs +3 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
- package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
- package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
- package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
- package/vendor/heyspex-mcp/vault-io.mjs +720 -724
- package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
- package/vendor/heyspex-mcp/whoami.mjs +2 -1
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
package/dist/cli/hs-init.mjs
CHANGED
|
@@ -1,25 +1,71 @@
|
|
|
1
1
|
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
2
|
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
4
15
|
var __esm = (fn, res) => function __init() {
|
|
5
16
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
17
|
};
|
|
18
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
19
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
// scripts/lib/heyspex-home.mjs
|
|
39
|
+
import { homedir } from "node:os";
|
|
40
|
+
function heyspexHome(env = process.env) {
|
|
41
|
+
const override = env?.HEYSPEX_HOME;
|
|
42
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
43
|
+
return override.trim();
|
|
44
|
+
}
|
|
45
|
+
return homedir();
|
|
46
|
+
}
|
|
47
|
+
var init_heyspex_home = __esm({
|
|
48
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
}
|
|
51
|
+
});
|
|
7
52
|
|
|
8
53
|
// vendor/heyspex-mcp/credentials.mjs
|
|
9
54
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
-
import os, { homedir } from "node:os";
|
|
55
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
11
56
|
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
12
57
|
var init_credentials = __esm({
|
|
13
58
|
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
14
59
|
"use strict";
|
|
60
|
+
init_heyspex_home();
|
|
15
61
|
}
|
|
16
62
|
});
|
|
17
63
|
|
|
18
64
|
// scripts/lib/dev-mode.mjs
|
|
19
65
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20
|
-
import { homedir as
|
|
66
|
+
import { homedir as homedir3 } from "node:os";
|
|
21
67
|
import { join as join2 } from "node:path";
|
|
22
|
-
function devModeFilePath(homeDir =
|
|
68
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
23
69
|
return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
24
70
|
}
|
|
25
71
|
function normalizeMode(raw) {
|
|
@@ -44,7 +90,7 @@ function deriveSemantics(mode, reliance) {
|
|
|
44
90
|
vaultWriteMode: local ? "local" : "cloud"
|
|
45
91
|
};
|
|
46
92
|
}
|
|
47
|
-
function readLocalDevModeRecord({ homeDir =
|
|
93
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
48
94
|
let raw;
|
|
49
95
|
try {
|
|
50
96
|
raw = readFile(devModeFilePath(homeDir));
|
|
@@ -74,7 +120,7 @@ function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFi
|
|
|
74
120
|
}
|
|
75
121
|
function resolveDevMode({
|
|
76
122
|
env = process.env,
|
|
77
|
-
homeDir =
|
|
123
|
+
homeDir = heyspexHome(),
|
|
78
124
|
readFile = defaultReadFile,
|
|
79
125
|
cloudSetting = void 0
|
|
80
126
|
} = {}) {
|
|
@@ -137,6 +183,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
|
137
183
|
var init_dev_mode = __esm({
|
|
138
184
|
"scripts/lib/dev-mode.mjs"() {
|
|
139
185
|
"use strict";
|
|
186
|
+
init_heyspex_home();
|
|
140
187
|
init_credentials();
|
|
141
188
|
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
142
189
|
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
@@ -145,20 +192,681 @@ var init_dev_mode = __esm({
|
|
|
145
192
|
}
|
|
146
193
|
});
|
|
147
194
|
|
|
195
|
+
// node_modules/kind-of/index.js
|
|
196
|
+
var require_kind_of = __commonJS({
|
|
197
|
+
"node_modules/kind-of/index.js"(exports2, module2) {
|
|
198
|
+
"use strict";
|
|
199
|
+
var toString = Object.prototype.toString;
|
|
200
|
+
module2.exports = function kindOf(val) {
|
|
201
|
+
if (val === void 0) return "undefined";
|
|
202
|
+
if (val === null) return "null";
|
|
203
|
+
var type = typeof val;
|
|
204
|
+
if (type === "boolean") return "boolean";
|
|
205
|
+
if (type === "string") return "string";
|
|
206
|
+
if (type === "number") return "number";
|
|
207
|
+
if (type === "symbol") return "symbol";
|
|
208
|
+
if (type === "function") {
|
|
209
|
+
return isGeneratorFn(val) ? "generatorfunction" : "function";
|
|
210
|
+
}
|
|
211
|
+
if (isArray(val)) return "array";
|
|
212
|
+
if (isBuffer(val)) return "buffer";
|
|
213
|
+
if (isArguments(val)) return "arguments";
|
|
214
|
+
if (isDate(val)) return "date";
|
|
215
|
+
if (isError(val)) return "error";
|
|
216
|
+
if (isRegexp(val)) return "regexp";
|
|
217
|
+
switch (ctorName(val)) {
|
|
218
|
+
case "Symbol":
|
|
219
|
+
return "symbol";
|
|
220
|
+
case "Promise":
|
|
221
|
+
return "promise";
|
|
222
|
+
// Set, Map, WeakSet, WeakMap
|
|
223
|
+
case "WeakMap":
|
|
224
|
+
return "weakmap";
|
|
225
|
+
case "WeakSet":
|
|
226
|
+
return "weakset";
|
|
227
|
+
case "Map":
|
|
228
|
+
return "map";
|
|
229
|
+
case "Set":
|
|
230
|
+
return "set";
|
|
231
|
+
// 8-bit typed arrays
|
|
232
|
+
case "Int8Array":
|
|
233
|
+
return "int8array";
|
|
234
|
+
case "Uint8Array":
|
|
235
|
+
return "uint8array";
|
|
236
|
+
case "Uint8ClampedArray":
|
|
237
|
+
return "uint8clampedarray";
|
|
238
|
+
// 16-bit typed arrays
|
|
239
|
+
case "Int16Array":
|
|
240
|
+
return "int16array";
|
|
241
|
+
case "Uint16Array":
|
|
242
|
+
return "uint16array";
|
|
243
|
+
// 32-bit typed arrays
|
|
244
|
+
case "Int32Array":
|
|
245
|
+
return "int32array";
|
|
246
|
+
case "Uint32Array":
|
|
247
|
+
return "uint32array";
|
|
248
|
+
case "Float32Array":
|
|
249
|
+
return "float32array";
|
|
250
|
+
case "Float64Array":
|
|
251
|
+
return "float64array";
|
|
252
|
+
}
|
|
253
|
+
if (isGeneratorObj(val)) {
|
|
254
|
+
return "generator";
|
|
255
|
+
}
|
|
256
|
+
type = toString.call(val);
|
|
257
|
+
switch (type) {
|
|
258
|
+
case "[object Object]":
|
|
259
|
+
return "object";
|
|
260
|
+
// iterators
|
|
261
|
+
case "[object Map Iterator]":
|
|
262
|
+
return "mapiterator";
|
|
263
|
+
case "[object Set Iterator]":
|
|
264
|
+
return "setiterator";
|
|
265
|
+
case "[object String Iterator]":
|
|
266
|
+
return "stringiterator";
|
|
267
|
+
case "[object Array Iterator]":
|
|
268
|
+
return "arrayiterator";
|
|
269
|
+
}
|
|
270
|
+
return type.slice(8, -1).toLowerCase().replace(/\s/g, "");
|
|
271
|
+
};
|
|
272
|
+
function ctorName(val) {
|
|
273
|
+
return typeof val.constructor === "function" ? val.constructor.name : null;
|
|
274
|
+
}
|
|
275
|
+
function isArray(val) {
|
|
276
|
+
if (Array.isArray) return Array.isArray(val);
|
|
277
|
+
return val instanceof Array;
|
|
278
|
+
}
|
|
279
|
+
function isError(val) {
|
|
280
|
+
return val instanceof Error || typeof val.message === "string" && val.constructor && typeof val.constructor.stackTraceLimit === "number";
|
|
281
|
+
}
|
|
282
|
+
function isDate(val) {
|
|
283
|
+
if (val instanceof Date) return true;
|
|
284
|
+
return typeof val.toDateString === "function" && typeof val.getDate === "function" && typeof val.setDate === "function";
|
|
285
|
+
}
|
|
286
|
+
function isRegexp(val) {
|
|
287
|
+
if (val instanceof RegExp) return true;
|
|
288
|
+
return typeof val.flags === "string" && typeof val.ignoreCase === "boolean" && typeof val.multiline === "boolean" && typeof val.global === "boolean";
|
|
289
|
+
}
|
|
290
|
+
function isGeneratorFn(name, val) {
|
|
291
|
+
return ctorName(name) === "GeneratorFunction";
|
|
292
|
+
}
|
|
293
|
+
function isGeneratorObj(val) {
|
|
294
|
+
return typeof val.throw === "function" && typeof val.return === "function" && typeof val.next === "function";
|
|
295
|
+
}
|
|
296
|
+
function isArguments(val) {
|
|
297
|
+
try {
|
|
298
|
+
if (typeof val.length === "number" && typeof val.callee === "function") {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (err.message.indexOf("callee") !== -1) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
function isBuffer(val) {
|
|
309
|
+
if (val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
310
|
+
return val.constructor.isBuffer(val);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// node_modules/is-extendable/index.js
|
|
318
|
+
var require_is_extendable = __commonJS({
|
|
319
|
+
"node_modules/is-extendable/index.js"(exports2, module2) {
|
|
320
|
+
"use strict";
|
|
321
|
+
module2.exports = function isExtendable(val) {
|
|
322
|
+
return typeof val !== "undefined" && val !== null && (typeof val === "object" || typeof val === "function");
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// node_modules/extend-shallow/index.js
|
|
328
|
+
var require_extend_shallow = __commonJS({
|
|
329
|
+
"node_modules/extend-shallow/index.js"(exports2, module2) {
|
|
330
|
+
"use strict";
|
|
331
|
+
var isObject = require_is_extendable();
|
|
332
|
+
module2.exports = function extend(o) {
|
|
333
|
+
if (!isObject(o)) {
|
|
334
|
+
o = {};
|
|
335
|
+
}
|
|
336
|
+
var len = arguments.length;
|
|
337
|
+
for (var i = 1; i < len; i++) {
|
|
338
|
+
var obj = arguments[i];
|
|
339
|
+
if (isObject(obj)) {
|
|
340
|
+
assign(o, obj);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return o;
|
|
344
|
+
};
|
|
345
|
+
function assign(a, b) {
|
|
346
|
+
for (var key in b) {
|
|
347
|
+
if (hasOwn(b, key)) {
|
|
348
|
+
a[key] = b[key];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function hasOwn(obj, key) {
|
|
353
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// node_modules/section-matter/index.js
|
|
359
|
+
var require_section_matter = __commonJS({
|
|
360
|
+
"node_modules/section-matter/index.js"(exports2, module2) {
|
|
361
|
+
"use strict";
|
|
362
|
+
var typeOf = require_kind_of();
|
|
363
|
+
var extend = require_extend_shallow();
|
|
364
|
+
module2.exports = function(input, options2) {
|
|
365
|
+
if (typeof options2 === "function") {
|
|
366
|
+
options2 = { parse: options2 };
|
|
367
|
+
}
|
|
368
|
+
var file = toObject(input);
|
|
369
|
+
var defaults = { section_delimiter: "---", parse: identity };
|
|
370
|
+
var opts = extend({}, defaults, options2);
|
|
371
|
+
var delim = opts.section_delimiter;
|
|
372
|
+
var lines = file.content.split(/\r?\n/);
|
|
373
|
+
var sections = null;
|
|
374
|
+
var section = createSection();
|
|
375
|
+
var content = [];
|
|
376
|
+
var stack = [];
|
|
377
|
+
function initSections(val) {
|
|
378
|
+
file.content = val;
|
|
379
|
+
sections = [];
|
|
380
|
+
content = [];
|
|
381
|
+
}
|
|
382
|
+
function closeSection(val) {
|
|
383
|
+
if (stack.length) {
|
|
384
|
+
section.key = getKey(stack[0], delim);
|
|
385
|
+
section.content = val;
|
|
386
|
+
opts.parse(section, sections);
|
|
387
|
+
sections.push(section);
|
|
388
|
+
section = createSection();
|
|
389
|
+
content = [];
|
|
390
|
+
stack = [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (var i = 0; i < lines.length; i++) {
|
|
394
|
+
var line = lines[i];
|
|
395
|
+
var len = stack.length;
|
|
396
|
+
var ln = line.trim();
|
|
397
|
+
if (isDelimiter(ln, delim)) {
|
|
398
|
+
if (ln.length === 3 && i !== 0) {
|
|
399
|
+
if (len === 0 || len === 2) {
|
|
400
|
+
content.push(line);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
stack.push(ln);
|
|
404
|
+
section.data = content.join("\n");
|
|
405
|
+
content = [];
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (sections === null) {
|
|
409
|
+
initSections(content.join("\n"));
|
|
410
|
+
}
|
|
411
|
+
if (len === 2) {
|
|
412
|
+
closeSection(content.join("\n"));
|
|
413
|
+
}
|
|
414
|
+
stack.push(ln);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
content.push(line);
|
|
418
|
+
}
|
|
419
|
+
if (sections === null) {
|
|
420
|
+
initSections(content.join("\n"));
|
|
421
|
+
} else {
|
|
422
|
+
closeSection(content.join("\n"));
|
|
423
|
+
}
|
|
424
|
+
file.sections = sections;
|
|
425
|
+
return file;
|
|
426
|
+
};
|
|
427
|
+
function isDelimiter(line, delim) {
|
|
428
|
+
if (line.slice(0, delim.length) !== delim) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (line.charAt(delim.length + 1) === delim.slice(-1)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
function toObject(input) {
|
|
437
|
+
if (typeOf(input) !== "object") {
|
|
438
|
+
input = { content: input };
|
|
439
|
+
}
|
|
440
|
+
if (typeof input.content !== "string" && !isBuffer(input.content)) {
|
|
441
|
+
throw new TypeError("expected a buffer or string");
|
|
442
|
+
}
|
|
443
|
+
input.content = input.content.toString();
|
|
444
|
+
input.sections = [];
|
|
445
|
+
return input;
|
|
446
|
+
}
|
|
447
|
+
function getKey(val, delim) {
|
|
448
|
+
return val ? val.slice(delim.length).trim() : "";
|
|
449
|
+
}
|
|
450
|
+
function createSection() {
|
|
451
|
+
return { key: "", data: "", content: "" };
|
|
452
|
+
}
|
|
453
|
+
function identity(val) {
|
|
454
|
+
return val;
|
|
455
|
+
}
|
|
456
|
+
function isBuffer(val) {
|
|
457
|
+
if (val && val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
458
|
+
return val.constructor.isBuffer(val);
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// node_modules/gray-matter/lib/engines.js
|
|
466
|
+
var require_engines = __commonJS({
|
|
467
|
+
"node_modules/gray-matter/lib/engines.js"(exports, module) {
|
|
468
|
+
"use strict";
|
|
469
|
+
var yaml = __require("js-yaml");
|
|
470
|
+
var engines = exports = module.exports;
|
|
471
|
+
engines.yaml = {
|
|
472
|
+
parse: yaml.safeLoad.bind(yaml),
|
|
473
|
+
stringify: yaml.safeDump.bind(yaml)
|
|
474
|
+
};
|
|
475
|
+
engines.json = {
|
|
476
|
+
parse: JSON.parse.bind(JSON),
|
|
477
|
+
stringify: function(obj, options2) {
|
|
478
|
+
const opts = Object.assign({ replacer: null, space: 2 }, options2);
|
|
479
|
+
return JSON.stringify(obj, opts.replacer, opts.space);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
engines.javascript = {
|
|
483
|
+
parse: function parse(str, options, wrap) {
|
|
484
|
+
try {
|
|
485
|
+
if (wrap !== false) {
|
|
486
|
+
str = "(function() {\nreturn " + str.trim() + ";\n}());";
|
|
487
|
+
}
|
|
488
|
+
return eval(str) || {};
|
|
489
|
+
} catch (err) {
|
|
490
|
+
if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
|
|
491
|
+
return parse(str, options, false);
|
|
492
|
+
}
|
|
493
|
+
throw new SyntaxError(err);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
stringify: function() {
|
|
497
|
+
throw new Error("stringifying JavaScript is not supported");
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// node_modules/strip-bom-string/index.js
|
|
504
|
+
var require_strip_bom_string = __commonJS({
|
|
505
|
+
"node_modules/strip-bom-string/index.js"(exports2, module2) {
|
|
506
|
+
"use strict";
|
|
507
|
+
module2.exports = function(str2) {
|
|
508
|
+
if (typeof str2 === "string" && str2.charAt(0) === "\uFEFF") {
|
|
509
|
+
return str2.slice(1);
|
|
510
|
+
}
|
|
511
|
+
return str2;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// node_modules/gray-matter/lib/utils.js
|
|
517
|
+
var require_utils = __commonJS({
|
|
518
|
+
"node_modules/gray-matter/lib/utils.js"(exports2) {
|
|
519
|
+
"use strict";
|
|
520
|
+
var stripBom = require_strip_bom_string();
|
|
521
|
+
var typeOf = require_kind_of();
|
|
522
|
+
exports2.define = function(obj, key, val) {
|
|
523
|
+
Reflect.defineProperty(obj, key, {
|
|
524
|
+
enumerable: false,
|
|
525
|
+
configurable: true,
|
|
526
|
+
writable: true,
|
|
527
|
+
value: val
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
exports2.isBuffer = function(val) {
|
|
531
|
+
return typeOf(val) === "buffer";
|
|
532
|
+
};
|
|
533
|
+
exports2.isObject = function(val) {
|
|
534
|
+
return typeOf(val) === "object";
|
|
535
|
+
};
|
|
536
|
+
exports2.toBuffer = function(input) {
|
|
537
|
+
return typeof input === "string" ? Buffer.from(input) : input;
|
|
538
|
+
};
|
|
539
|
+
exports2.toString = function(input) {
|
|
540
|
+
if (exports2.isBuffer(input)) return stripBom(String(input));
|
|
541
|
+
if (typeof input !== "string") {
|
|
542
|
+
throw new TypeError("expected input to be a string or buffer");
|
|
543
|
+
}
|
|
544
|
+
return stripBom(input);
|
|
545
|
+
};
|
|
546
|
+
exports2.arrayify = function(val) {
|
|
547
|
+
return val ? Array.isArray(val) ? val : [val] : [];
|
|
548
|
+
};
|
|
549
|
+
exports2.startsWith = function(str2, substr, len) {
|
|
550
|
+
if (typeof len !== "number") len = substr.length;
|
|
551
|
+
return str2.slice(0, len) === substr;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// node_modules/gray-matter/lib/defaults.js
|
|
557
|
+
var require_defaults = __commonJS({
|
|
558
|
+
"node_modules/gray-matter/lib/defaults.js"(exports2, module2) {
|
|
559
|
+
"use strict";
|
|
560
|
+
var engines2 = require_engines();
|
|
561
|
+
var utils = require_utils();
|
|
562
|
+
module2.exports = function(options2) {
|
|
563
|
+
const opts = Object.assign({}, options2);
|
|
564
|
+
opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || "---");
|
|
565
|
+
if (opts.delimiters.length === 1) {
|
|
566
|
+
opts.delimiters.push(opts.delimiters[0]);
|
|
567
|
+
}
|
|
568
|
+
opts.language = (opts.language || opts.lang || "yaml").toLowerCase();
|
|
569
|
+
opts.engines = Object.assign({}, engines2, opts.parsers, opts.engines);
|
|
570
|
+
return opts;
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// node_modules/gray-matter/lib/engine.js
|
|
576
|
+
var require_engine = __commonJS({
|
|
577
|
+
"node_modules/gray-matter/lib/engine.js"(exports2, module2) {
|
|
578
|
+
"use strict";
|
|
579
|
+
module2.exports = function(name, options2) {
|
|
580
|
+
let engine = options2.engines[name] || options2.engines[aliase(name)];
|
|
581
|
+
if (typeof engine === "undefined") {
|
|
582
|
+
throw new Error('gray-matter engine "' + name + '" is not registered');
|
|
583
|
+
}
|
|
584
|
+
if (typeof engine === "function") {
|
|
585
|
+
engine = { parse: engine };
|
|
586
|
+
}
|
|
587
|
+
return engine;
|
|
588
|
+
};
|
|
589
|
+
function aliase(name) {
|
|
590
|
+
switch (name.toLowerCase()) {
|
|
591
|
+
case "js":
|
|
592
|
+
case "javascript":
|
|
593
|
+
return "javascript";
|
|
594
|
+
case "coffee":
|
|
595
|
+
case "coffeescript":
|
|
596
|
+
case "cson":
|
|
597
|
+
return "coffee";
|
|
598
|
+
case "yaml":
|
|
599
|
+
case "yml":
|
|
600
|
+
return "yaml";
|
|
601
|
+
default: {
|
|
602
|
+
return name;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// node_modules/gray-matter/lib/stringify.js
|
|
610
|
+
var require_stringify = __commonJS({
|
|
611
|
+
"node_modules/gray-matter/lib/stringify.js"(exports2, module2) {
|
|
612
|
+
"use strict";
|
|
613
|
+
var typeOf = require_kind_of();
|
|
614
|
+
var getEngine = require_engine();
|
|
615
|
+
var defaults = require_defaults();
|
|
616
|
+
module2.exports = function(file, data, options2) {
|
|
617
|
+
if (data == null && options2 == null) {
|
|
618
|
+
switch (typeOf(file)) {
|
|
619
|
+
case "object":
|
|
620
|
+
data = file.data;
|
|
621
|
+
options2 = {};
|
|
622
|
+
break;
|
|
623
|
+
case "string":
|
|
624
|
+
return file;
|
|
625
|
+
default: {
|
|
626
|
+
throw new TypeError("expected file to be a string or object");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const str2 = file.content;
|
|
631
|
+
const opts = defaults(options2);
|
|
632
|
+
if (data == null) {
|
|
633
|
+
if (!opts.data) return file;
|
|
634
|
+
data = opts.data;
|
|
635
|
+
}
|
|
636
|
+
const language = file.language || opts.language;
|
|
637
|
+
const engine = getEngine(language, opts);
|
|
638
|
+
if (typeof engine.stringify !== "function") {
|
|
639
|
+
throw new TypeError('expected "' + language + '.stringify" to be a function');
|
|
640
|
+
}
|
|
641
|
+
data = Object.assign({}, file.data, data);
|
|
642
|
+
const open = opts.delimiters[0];
|
|
643
|
+
const close = opts.delimiters[1];
|
|
644
|
+
const matter2 = engine.stringify(data, options2).trim();
|
|
645
|
+
let buf = "";
|
|
646
|
+
if (matter2 !== "{}") {
|
|
647
|
+
buf = newline(open) + newline(matter2) + newline(close);
|
|
648
|
+
}
|
|
649
|
+
if (typeof file.excerpt === "string" && file.excerpt !== "") {
|
|
650
|
+
if (str2.indexOf(file.excerpt.trim()) === -1) {
|
|
651
|
+
buf += newline(file.excerpt) + newline(close);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return buf + newline(str2);
|
|
655
|
+
};
|
|
656
|
+
function newline(str2) {
|
|
657
|
+
return str2.slice(-1) !== "\n" ? str2 + "\n" : str2;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// node_modules/gray-matter/lib/excerpt.js
|
|
663
|
+
var require_excerpt = __commonJS({
|
|
664
|
+
"node_modules/gray-matter/lib/excerpt.js"(exports2, module2) {
|
|
665
|
+
"use strict";
|
|
666
|
+
var defaults = require_defaults();
|
|
667
|
+
module2.exports = function(file, options2) {
|
|
668
|
+
const opts = defaults(options2);
|
|
669
|
+
if (file.data == null) {
|
|
670
|
+
file.data = {};
|
|
671
|
+
}
|
|
672
|
+
if (typeof opts.excerpt === "function") {
|
|
673
|
+
return opts.excerpt(file, opts);
|
|
674
|
+
}
|
|
675
|
+
const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
|
|
676
|
+
if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
|
|
677
|
+
return file;
|
|
678
|
+
}
|
|
679
|
+
const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
|
|
680
|
+
const idx = file.content.indexOf(delimiter2);
|
|
681
|
+
if (idx !== -1) {
|
|
682
|
+
file.excerpt = file.content.slice(0, idx);
|
|
683
|
+
}
|
|
684
|
+
return file;
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// node_modules/gray-matter/lib/to-file.js
|
|
690
|
+
var require_to_file = __commonJS({
|
|
691
|
+
"node_modules/gray-matter/lib/to-file.js"(exports2, module2) {
|
|
692
|
+
"use strict";
|
|
693
|
+
var typeOf = require_kind_of();
|
|
694
|
+
var stringify = require_stringify();
|
|
695
|
+
var utils = require_utils();
|
|
696
|
+
module2.exports = function(file) {
|
|
697
|
+
if (typeOf(file) !== "object") {
|
|
698
|
+
file = { content: file };
|
|
699
|
+
}
|
|
700
|
+
if (typeOf(file.data) !== "object") {
|
|
701
|
+
file.data = {};
|
|
702
|
+
}
|
|
703
|
+
if (file.contents && file.content == null) {
|
|
704
|
+
file.content = file.contents;
|
|
705
|
+
}
|
|
706
|
+
utils.define(file, "orig", utils.toBuffer(file.content));
|
|
707
|
+
utils.define(file, "language", file.language || "");
|
|
708
|
+
utils.define(file, "matter", file.matter || "");
|
|
709
|
+
utils.define(file, "stringify", function(data, options2) {
|
|
710
|
+
if (options2 && options2.language) {
|
|
711
|
+
file.language = options2.language;
|
|
712
|
+
}
|
|
713
|
+
return stringify(file, data, options2);
|
|
714
|
+
});
|
|
715
|
+
file.content = utils.toString(file.content);
|
|
716
|
+
file.isEmpty = false;
|
|
717
|
+
file.excerpt = "";
|
|
718
|
+
return file;
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// node_modules/gray-matter/lib/parse.js
|
|
724
|
+
var require_parse = __commonJS({
|
|
725
|
+
"node_modules/gray-matter/lib/parse.js"(exports2, module2) {
|
|
726
|
+
"use strict";
|
|
727
|
+
var getEngine = require_engine();
|
|
728
|
+
var defaults = require_defaults();
|
|
729
|
+
module2.exports = function(language, str2, options2) {
|
|
730
|
+
const opts = defaults(options2);
|
|
731
|
+
const engine = getEngine(language, opts);
|
|
732
|
+
if (typeof engine.parse !== "function") {
|
|
733
|
+
throw new TypeError('expected "' + language + '.parse" to be a function');
|
|
734
|
+
}
|
|
735
|
+
return engine.parse(str2, opts);
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// node_modules/gray-matter/index.js
|
|
741
|
+
var require_gray_matter = __commonJS({
|
|
742
|
+
"node_modules/gray-matter/index.js"(exports2, module2) {
|
|
743
|
+
"use strict";
|
|
744
|
+
var fs4 = __require("fs");
|
|
745
|
+
var sections = require_section_matter();
|
|
746
|
+
var defaults = require_defaults();
|
|
747
|
+
var stringify = require_stringify();
|
|
748
|
+
var excerpt = require_excerpt();
|
|
749
|
+
var engines2 = require_engines();
|
|
750
|
+
var toFile = require_to_file();
|
|
751
|
+
var parse2 = require_parse();
|
|
752
|
+
var utils = require_utils();
|
|
753
|
+
function matter2(input, options2) {
|
|
754
|
+
if (input === "") {
|
|
755
|
+
return { data: {}, content: input, excerpt: "", orig: input };
|
|
756
|
+
}
|
|
757
|
+
let file = toFile(input);
|
|
758
|
+
const cached = matter2.cache[file.content];
|
|
759
|
+
if (!options2) {
|
|
760
|
+
if (cached) {
|
|
761
|
+
file = Object.assign({}, cached);
|
|
762
|
+
file.orig = cached.orig;
|
|
763
|
+
return file;
|
|
764
|
+
}
|
|
765
|
+
matter2.cache[file.content] = file;
|
|
766
|
+
}
|
|
767
|
+
return parseMatter(file, options2);
|
|
768
|
+
}
|
|
769
|
+
function parseMatter(file, options2) {
|
|
770
|
+
const opts = defaults(options2);
|
|
771
|
+
const open = opts.delimiters[0];
|
|
772
|
+
const close = "\n" + opts.delimiters[1];
|
|
773
|
+
let str2 = file.content;
|
|
774
|
+
if (opts.language) {
|
|
775
|
+
file.language = opts.language;
|
|
776
|
+
}
|
|
777
|
+
const openLen = open.length;
|
|
778
|
+
if (!utils.startsWith(str2, open, openLen)) {
|
|
779
|
+
excerpt(file, opts);
|
|
780
|
+
return file;
|
|
781
|
+
}
|
|
782
|
+
if (str2.charAt(openLen) === open.slice(-1)) {
|
|
783
|
+
return file;
|
|
784
|
+
}
|
|
785
|
+
str2 = str2.slice(openLen);
|
|
786
|
+
const len = str2.length;
|
|
787
|
+
const language = matter2.language(str2, opts);
|
|
788
|
+
if (language.name) {
|
|
789
|
+
file.language = language.name;
|
|
790
|
+
str2 = str2.slice(language.raw.length);
|
|
791
|
+
}
|
|
792
|
+
let closeIndex = str2.indexOf(close);
|
|
793
|
+
if (closeIndex === -1) {
|
|
794
|
+
closeIndex = len;
|
|
795
|
+
}
|
|
796
|
+
file.matter = str2.slice(0, closeIndex);
|
|
797
|
+
const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
|
|
798
|
+
if (block === "") {
|
|
799
|
+
file.isEmpty = true;
|
|
800
|
+
file.empty = file.content;
|
|
801
|
+
file.data = {};
|
|
802
|
+
} else {
|
|
803
|
+
file.data = parse2(file.language, file.matter, opts);
|
|
804
|
+
}
|
|
805
|
+
if (closeIndex === len) {
|
|
806
|
+
file.content = "";
|
|
807
|
+
} else {
|
|
808
|
+
file.content = str2.slice(closeIndex + close.length);
|
|
809
|
+
if (file.content[0] === "\r") {
|
|
810
|
+
file.content = file.content.slice(1);
|
|
811
|
+
}
|
|
812
|
+
if (file.content[0] === "\n") {
|
|
813
|
+
file.content = file.content.slice(1);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
excerpt(file, opts);
|
|
817
|
+
if (opts.sections === true || typeof opts.section === "function") {
|
|
818
|
+
sections(file, opts.section);
|
|
819
|
+
}
|
|
820
|
+
return file;
|
|
821
|
+
}
|
|
822
|
+
matter2.engines = engines2;
|
|
823
|
+
matter2.stringify = function(file, data, options2) {
|
|
824
|
+
if (typeof file === "string") file = matter2(file, options2);
|
|
825
|
+
return stringify(file, data, options2);
|
|
826
|
+
};
|
|
827
|
+
matter2.read = function(filepath, options2) {
|
|
828
|
+
const str2 = fs4.readFileSync(filepath, "utf8");
|
|
829
|
+
const file = matter2(str2, options2);
|
|
830
|
+
file.path = filepath;
|
|
831
|
+
return file;
|
|
832
|
+
};
|
|
833
|
+
matter2.test = function(str2, options2) {
|
|
834
|
+
return utils.startsWith(str2, defaults(options2).delimiters[0]);
|
|
835
|
+
};
|
|
836
|
+
matter2.language = function(str2, options2) {
|
|
837
|
+
const opts = defaults(options2);
|
|
838
|
+
const open = opts.delimiters[0];
|
|
839
|
+
if (matter2.test(str2)) {
|
|
840
|
+
str2 = str2.slice(open.length);
|
|
841
|
+
}
|
|
842
|
+
const language = str2.slice(0, str2.search(/\r?\n/));
|
|
843
|
+
return {
|
|
844
|
+
raw: language,
|
|
845
|
+
name: language ? language.trim() : ""
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
matter2.cache = {};
|
|
849
|
+
matter2.clearCache = function() {
|
|
850
|
+
matter2.cache = {};
|
|
851
|
+
};
|
|
852
|
+
module2.exports = matter2;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
148
856
|
// spex-console/bin/hs-init.ts
|
|
149
857
|
import {
|
|
150
858
|
copyFileSync,
|
|
151
|
-
existsSync as
|
|
152
|
-
mkdirSync as
|
|
153
|
-
readdirSync,
|
|
154
|
-
readFileSync as
|
|
859
|
+
existsSync as existsSync5,
|
|
860
|
+
mkdirSync as mkdirSync6,
|
|
861
|
+
readdirSync as readdirSync2,
|
|
862
|
+
readFileSync as readFileSync11,
|
|
155
863
|
statSync,
|
|
156
|
-
writeFileSync as
|
|
864
|
+
writeFileSync as writeFileSync6
|
|
157
865
|
} from "node:fs";
|
|
158
|
-
import { dirname as
|
|
159
|
-
import
|
|
866
|
+
import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
|
|
867
|
+
import process4 from "node:process";
|
|
160
868
|
import { createInterface } from "node:readline/promises";
|
|
161
|
-
import { fileURLToPath as
|
|
869
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
162
870
|
|
|
163
871
|
// scripts/lib/shared-path-helpers.mjs
|
|
164
872
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
@@ -228,13 +936,13 @@ The vault under \`specs/\` is the only persistent queue. File new work via \`hey
|
|
|
228
936
|
This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
|
|
229
937
|
`;
|
|
230
938
|
}
|
|
231
|
-
function writeProjectContract(ctx,
|
|
939
|
+
function writeProjectContract(ctx, options2 = {}) {
|
|
232
940
|
const content = renderCustomerClaudeMd({
|
|
233
941
|
vaultName: ctx.vaultName,
|
|
234
942
|
templateVersion: ctx.templateVersion
|
|
235
943
|
});
|
|
236
944
|
const results = [];
|
|
237
|
-
for (const file of
|
|
945
|
+
for (const file of options2.files ?? ["CLAUDE.md"]) {
|
|
238
946
|
const destPath = ctx.deps.joinPath(ctx.targetDir, file);
|
|
239
947
|
if (!ctx.deps.exists(destPath)) {
|
|
240
948
|
ctx.deps.writeFile(destPath, content);
|
|
@@ -252,7 +960,7 @@ function writeProjectContract(ctx, options = {}) {
|
|
|
252
960
|
ctx.recordWrite?.(templatePath);
|
|
253
961
|
}
|
|
254
962
|
const hint = `${file} already exists - left untouched. Generated contract written to ${file}.template; compare with: diff ${file} ${file}.template`;
|
|
255
|
-
|
|
963
|
+
options2.notify?.(hint);
|
|
256
964
|
results.push({ file, action: "template", path: templatePath, hint });
|
|
257
965
|
}
|
|
258
966
|
return results;
|
|
@@ -267,9 +975,10 @@ function customerClaudeMdHook(notify) {
|
|
|
267
975
|
import { randomUUID } from "node:crypto";
|
|
268
976
|
|
|
269
977
|
// scripts/lib/console-url-resolver.mjs
|
|
978
|
+
init_heyspex_home();
|
|
270
979
|
init_dev_mode();
|
|
271
980
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
272
|
-
import { homedir as
|
|
981
|
+
import { homedir as homedir4 } from "node:os";
|
|
273
982
|
import { join as join3 } from "node:path";
|
|
274
983
|
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
275
984
|
function trimTrailingSlash(url) {
|
|
@@ -296,7 +1005,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
|
296
1005
|
} catch {
|
|
297
1006
|
}
|
|
298
1007
|
}
|
|
299
|
-
function readCredentialsConsoleUrl({ homeDir =
|
|
1008
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
300
1009
|
try {
|
|
301
1010
|
const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
|
|
302
1011
|
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
@@ -340,7 +1049,7 @@ function resolveModeEnvelope({ env, homeDir }) {
|
|
|
340
1049
|
function resolveConsoleUrl({
|
|
341
1050
|
repoRoot,
|
|
342
1051
|
env = process.env,
|
|
343
|
-
homeDir =
|
|
1052
|
+
homeDir = heyspexHome(),
|
|
344
1053
|
strict = false,
|
|
345
1054
|
devMode,
|
|
346
1055
|
log = (line) => process.stderr.write(line),
|
|
@@ -392,15 +1101,17 @@ function shellQuote(p) {
|
|
|
392
1101
|
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
393
1102
|
|
|
394
1103
|
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1104
|
+
init_heyspex_home();
|
|
395
1105
|
init_credentials();
|
|
396
|
-
import { readFileSync as
|
|
397
|
-
import { homedir as
|
|
398
|
-
import { join as
|
|
1106
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1107
|
+
import { homedir as homedir7 } from "node:os";
|
|
1108
|
+
import { join as join8 } from "node:path";
|
|
399
1109
|
|
|
400
1110
|
// vendor/heyspex-mcp/session-owner.mjs
|
|
1111
|
+
init_heyspex_home();
|
|
401
1112
|
init_credentials();
|
|
402
1113
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
403
|
-
import { homedir as
|
|
1114
|
+
import { homedir as homedir5 } from "node:os";
|
|
404
1115
|
import { join as join5 } from "node:path";
|
|
405
1116
|
|
|
406
1117
|
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
@@ -414,10 +1125,23 @@ var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
|
414
1125
|
"* /api/admin/dev-mode"
|
|
415
1126
|
]);
|
|
416
1127
|
|
|
1128
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
1129
|
+
import {
|
|
1130
|
+
readdirSync,
|
|
1131
|
+
readFileSync as readFileSync5,
|
|
1132
|
+
writeFileSync as writeFileSync3,
|
|
1133
|
+
mkdirSync as mkdirSync3,
|
|
1134
|
+
existsSync as existsSync2,
|
|
1135
|
+
renameSync as renameSync2,
|
|
1136
|
+
rmSync
|
|
1137
|
+
} from "node:fs";
|
|
1138
|
+
import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
|
|
1139
|
+
|
|
417
1140
|
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
418
|
-
|
|
419
|
-
import {
|
|
420
|
-
import {
|
|
1141
|
+
init_heyspex_home();
|
|
1142
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
1143
|
+
import { homedir as homedir6, tmpdir } from "node:os";
|
|
1144
|
+
import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
|
|
421
1145
|
|
|
422
1146
|
// vendor/heyspex-mcp/console-fetch.mjs
|
|
423
1147
|
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
@@ -426,9 +1150,11 @@ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMAT
|
|
|
426
1150
|
init_credentials();
|
|
427
1151
|
|
|
428
1152
|
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
429
|
-
import { existsSync as
|
|
430
|
-
import {
|
|
431
|
-
|
|
1153
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1154
|
+
import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
|
|
1155
|
+
|
|
1156
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
1157
|
+
import { homedir as homedir8 } from "node:os";
|
|
432
1158
|
|
|
433
1159
|
// spex-console/src/lib/cli/control-shared.ts
|
|
434
1160
|
var EXIT = {
|
|
@@ -626,10 +1352,10 @@ function parseInitArgs(argv) {
|
|
|
626
1352
|
}
|
|
627
1353
|
var HEYSPEX_CONFIG_DIR = ".heyspex";
|
|
628
1354
|
var HEYSPEX_CONFIG_FILE = "config.yaml";
|
|
629
|
-
function seedConfigYaml(root, readSeed,
|
|
630
|
-
const configDir =
|
|
631
|
-
const configPath =
|
|
632
|
-
if (
|
|
1355
|
+
function seedConfigYaml(root, readSeed, fs4) {
|
|
1356
|
+
const configDir = fs4.joinPath(root, HEYSPEX_CONFIG_DIR);
|
|
1357
|
+
const configPath = fs4.joinPath(configDir, HEYSPEX_CONFIG_FILE);
|
|
1358
|
+
if (fs4.exists(configPath)) return { status: "kept", path: configPath };
|
|
633
1359
|
if (!readSeed) return { status: "no_seed", path: configPath };
|
|
634
1360
|
let seed;
|
|
635
1361
|
try {
|
|
@@ -638,8 +1364,8 @@ function seedConfigYaml(root, readSeed, fs) {
|
|
|
638
1364
|
seed = null;
|
|
639
1365
|
}
|
|
640
1366
|
if (seed === null) return { status: "no_seed", path: configPath };
|
|
641
|
-
|
|
642
|
-
|
|
1367
|
+
fs4.mkdirRecursive(configDir);
|
|
1368
|
+
fs4.writeFile(configPath, seed);
|
|
643
1369
|
return { status: "seeded", path: configPath };
|
|
644
1370
|
}
|
|
645
1371
|
function renderHeyspexYaml(args) {
|
|
@@ -765,7 +1491,7 @@ function runInit(args, deps) {
|
|
|
765
1491
|
};
|
|
766
1492
|
}
|
|
767
1493
|
const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
|
|
768
|
-
const accountId =
|
|
1494
|
+
const accountId = null;
|
|
769
1495
|
const content = renderHeyspexYaml({ ...args, vaultId, accountId });
|
|
770
1496
|
try {
|
|
771
1497
|
deps.writeFile(markerPath, content);
|
|
@@ -849,7 +1575,7 @@ var HELP = [
|
|
|
849
1575
|
].join("\n");
|
|
850
1576
|
|
|
851
1577
|
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
852
|
-
import { basename } from "node:path";
|
|
1578
|
+
import { basename as basename2 } from "node:path";
|
|
853
1579
|
|
|
854
1580
|
// spex-console/src/lib/cli/template-manifest.sources.mjs
|
|
855
1581
|
var UNIVERSAL_TEMPLATE_ROWS = [
|
|
@@ -1335,10 +2061,10 @@ var SCAFFOLD_FOLDERS = [
|
|
|
1335
2061
|
"events"
|
|
1336
2062
|
];
|
|
1337
2063
|
var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
|
|
1338
|
-
function isDir(deps,
|
|
1339
|
-
if (deps.isDirectory) return deps.isDirectory(
|
|
2064
|
+
function isDir(deps, path5, name) {
|
|
2065
|
+
if (deps.isDirectory) return deps.isDirectory(path5);
|
|
1340
2066
|
try {
|
|
1341
|
-
const kids = deps.readdir(
|
|
2067
|
+
const kids = deps.readdir(path5);
|
|
1342
2068
|
return kids.length > 0 || !name.includes(".");
|
|
1343
2069
|
} catch {
|
|
1344
2070
|
return false;
|
|
@@ -1360,12 +2086,12 @@ function findManagedSpecs(deps, targetDir, manifestDests) {
|
|
|
1360
2086
|
return out;
|
|
1361
2087
|
}
|
|
1362
2088
|
function defaultVaultName(targetDir) {
|
|
1363
|
-
return
|
|
2089
|
+
return basename2(targetDir) || targetDir;
|
|
1364
2090
|
}
|
|
1365
|
-
function runCustomerVaultInit(args, deps,
|
|
2091
|
+
function runCustomerVaultInit(args, deps, options2 = {}) {
|
|
1366
2092
|
const { targetDir } = args;
|
|
1367
2093
|
const written = [];
|
|
1368
|
-
const filter =
|
|
2094
|
+
const filter = options2.manifestFilter ?? (() => true);
|
|
1369
2095
|
const rows = TEMPLATE_MANIFEST.filter(filter);
|
|
1370
2096
|
if (deps.exists(targetDir)) {
|
|
1371
2097
|
const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
|
|
@@ -1409,7 +2135,7 @@ function runCustomerVaultInit(args, deps, options = {}) {
|
|
|
1409
2135
|
deps.writeFile(
|
|
1410
2136
|
heyspexYamlPath,
|
|
1411
2137
|
renderHeyspexYaml({
|
|
1412
|
-
consoleUrl:
|
|
2138
|
+
consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
|
|
1413
2139
|
vaultRoot: "specs/",
|
|
1414
2140
|
vaultId
|
|
1415
2141
|
})
|
|
@@ -1444,19 +2170,19 @@ function runCustomerVaultInit(args, deps, options = {}) {
|
|
|
1444
2170
|
written.push(destAbs);
|
|
1445
2171
|
rowResults.push({ dest: row.dest, action: "created" });
|
|
1446
2172
|
}
|
|
1447
|
-
if (
|
|
2173
|
+
if (options2.afterScaffold && options2.afterScaffold.length > 0) {
|
|
1448
2174
|
const ctx = {
|
|
1449
2175
|
targetDir,
|
|
1450
|
-
vaultName:
|
|
2176
|
+
vaultName: options2.vaultName ?? defaultVaultName(targetDir),
|
|
1451
2177
|
templateVersion: TEMPLATE_VERSION,
|
|
1452
2178
|
manifestRows: rows,
|
|
1453
2179
|
rows: rowResults,
|
|
1454
2180
|
deps,
|
|
1455
|
-
recordWrite: (
|
|
1456
|
-
written.push(
|
|
2181
|
+
recordWrite: (path5) => {
|
|
2182
|
+
written.push(path5);
|
|
1457
2183
|
}
|
|
1458
2184
|
};
|
|
1459
|
-
for (const hook of
|
|
2185
|
+
for (const hook of options2.afterScaffold) hook(ctx);
|
|
1460
2186
|
}
|
|
1461
2187
|
const createdCount = rowResults.filter((r) => r.action === "created").length;
|
|
1462
2188
|
const keptCount = rowResults.length - createdCount;
|
|
@@ -1544,15 +2270,1102 @@ function gitFinalizeHook(runner, notify) {
|
|
|
1544
2270
|
};
|
|
1545
2271
|
}
|
|
1546
2272
|
|
|
2273
|
+
// spex-console/src/lib/cli/install.ts
|
|
2274
|
+
import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
|
|
2275
|
+
|
|
2276
|
+
// scripts/install-cursor-hooks.mjs
|
|
2277
|
+
import fs2 from "node:fs";
|
|
2278
|
+
import path3 from "node:path";
|
|
2279
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2280
|
+
|
|
2281
|
+
// scripts/cursor-hooks/manifest.mjs
|
|
2282
|
+
var SHIM = "scripts/cursor-hooks/_run.mjs";
|
|
2283
|
+
var DEFAULT_TIMEOUT_SECONDS = 5;
|
|
2284
|
+
var HOOKS = [
|
|
2285
|
+
// ── Session start ───────────────────────────────────────────────────────
|
|
2286
|
+
// Cursor's sessionStart honors `additional_context`, so these injections
|
|
2287
|
+
// land in context exactly as they do in Claude Code.
|
|
2288
|
+
{ event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
|
|
2289
|
+
{ event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
|
|
2290
|
+
{ event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
|
|
2291
|
+
{ event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
|
|
2292
|
+
{ event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
|
|
2293
|
+
{ event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
|
|
2294
|
+
{ event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
|
|
2295
|
+
{ event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
|
|
2296
|
+
{ event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
|
|
2297
|
+
{ event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
|
|
2298
|
+
// ── Compaction ──────────────────────────────────────────────────────────
|
|
2299
|
+
// Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
|
|
2300
|
+
// preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
|
|
2301
|
+
// to keep wip-checkpoint's "rollover, not fresh start" framing.
|
|
2302
|
+
{ event: "preCompact", hook: "wip-checkpoint", claudeEvent: "SessionStart", why: "Carry the WIP handoff across a context compaction." },
|
|
2303
|
+
// ── Every user turn ─────────────────────────────────────────────────────
|
|
2304
|
+
// beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
|
|
2305
|
+
// but not `additional_context`, so these three surface in the transcript
|
|
2306
|
+
// rather than as invisible context — the routing still reaches the agent,
|
|
2307
|
+
// and the user can see why it fired.
|
|
2308
|
+
{ event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
|
|
2309
|
+
{ event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
|
|
2310
|
+
{ event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
|
|
2311
|
+
{ event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
|
|
2312
|
+
// ── Before every tool call (any tool) ───────────────────────────────────
|
|
2313
|
+
{ event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
|
|
2314
|
+
{ event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
|
|
2315
|
+
{ event: "preToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PreToolUse"], why: "Append agent.tool_called rows to the session log." },
|
|
2316
|
+
// ── Before a file write / delete ─────────────────────────────────────────
|
|
2317
|
+
// Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
|
|
2318
|
+
// the shim re-derives which one from the input shape. `Delete` is included on
|
|
2319
|
+
// the vault guard because removing a profile file is the loss it exists to
|
|
2320
|
+
// prevent, and Claude had no separate delete tool to list.
|
|
2321
|
+
{ event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
|
|
2322
|
+
{ event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
2323
|
+
{ event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
|
|
2324
|
+
{ event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
|
|
2325
|
+
{ event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
|
|
2326
|
+
{ event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
|
|
2327
|
+
{ event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
|
|
2328
|
+
{ event: "preToolUse", matcher: "Write", hook: "directive-obligation-gate", why: "Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW)." },
|
|
2329
|
+
// ── Before a shell command ──────────────────────────────────────────────
|
|
2330
|
+
// beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
|
|
2331
|
+
// to carry the command string, and Cursor honors allow/deny/ask on it.
|
|
2332
|
+
{ event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
|
|
2333
|
+
{ event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
2334
|
+
{ event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
|
|
2335
|
+
{ event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
|
|
2336
|
+
{ event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
|
|
2337
|
+
{ event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
|
|
2338
|
+
{ event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
|
|
2339
|
+
{ event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
|
|
2340
|
+
{ event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
|
|
2341
|
+
{ event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
|
|
2342
|
+
{ event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
|
|
2343
|
+
// ── Before specific MCP tools ───────────────────────────────────────────
|
|
2344
|
+
// Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
|
|
2345
|
+
// stay on preToolUse rather than beforeMCPExecution — registering both would
|
|
2346
|
+
// fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
|
|
2347
|
+
// matcher cannot name one MCP tool per entry portably.
|
|
2348
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_claim_spec$", hook: "coo-guard-check", why: "Block a claim whose breakdown was never delegated." },
|
|
2349
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "capture-workflow-gate", timeout: 8, why: "Deny filing an implementation spec straight out of an open capture." },
|
|
2350
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
|
|
2351
|
+
{ event: "preToolUse", matcher: ".*", tool: "[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$", hook: "dev-server-task-reminder", timeout: 10, why: "DB-safety gate on Neon migration/reset/delete tools." },
|
|
2352
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "diagnose-capture-reminder", why: "Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D)." },
|
|
2353
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
|
|
2354
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
|
|
2355
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$", hook: "research-protocol-gate", why: "WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F)." },
|
|
2356
|
+
// ── Before a subagent spawn ─────────────────────────────────────────────
|
|
2357
|
+
// subagentStart over preToolUse/Task: it carries subagent_type and the model,
|
|
2358
|
+
// which is exactly what the routing gate decides on, and it can deny.
|
|
2359
|
+
{ event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
|
|
2360
|
+
{ event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
|
|
2361
|
+
{ event: "subagentStart", hook: "directive-obligation-gate", why: "Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW)." },
|
|
2362
|
+
// ── After every tool call ───────────────────────────────────────────────
|
|
2363
|
+
{ event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
|
|
2364
|
+
{ event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
|
|
2365
|
+
{ event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
|
|
2366
|
+
{ event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
|
|
2367
|
+
{ event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
|
|
2368
|
+
{ event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
|
|
2369
|
+
// afterFileEdit is a distinct signal, not a duplicate: it reports the edit
|
|
2370
|
+
// that actually landed (with old/new strings) even when the write came from a
|
|
2371
|
+
// path postToolUse did not describe as an edit.
|
|
2372
|
+
{ event: "afterFileEdit", matcher: ".*", hook: "close-terminal-on-ship", why: "Ship detection from the concrete file edit." },
|
|
2373
|
+
// ── End of the agent turn ───────────────────────────────────────────────
|
|
2374
|
+
// Cursor's `stop` honors `followup_message`, which is how a blocking Stop
|
|
2375
|
+
// hook re-drives the agent — the same effect as Claude's exit-2 block.
|
|
2376
|
+
{ event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
|
|
2377
|
+
{ event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
|
|
2378
|
+
{ event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
|
|
2379
|
+
{ event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
|
|
2380
|
+
{ event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
|
|
2381
|
+
{ event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
|
|
2382
|
+
{ event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
|
|
2383
|
+
// ── Session end ─────────────────────────────────────────────────────────
|
|
2384
|
+
{ event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
|
|
2385
|
+
{ event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
|
|
2386
|
+
];
|
|
2387
|
+
var UNPORTABLE = [
|
|
2388
|
+
{
|
|
2389
|
+
hook: "plan-review-gate",
|
|
2390
|
+
claudeEvent: "PreToolUse/ExitPlanMode",
|
|
2391
|
+
reason: "Cursor exposes no ExitPlanMode tool call to hooks.",
|
|
2392
|
+
coveredBy: ".cursor/rules/plan-review-required.mdc (always-on rule)"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
hook: "plan-exit-ingest-gate",
|
|
2396
|
+
claudeEvent: "PreToolUse/ExitPlanMode",
|
|
2397
|
+
reason: "Same missing trigger; it is also what arms claim-before-code-gate.",
|
|
2398
|
+
coveredBy: ".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"
|
|
2399
|
+
}
|
|
2400
|
+
];
|
|
2401
|
+
function commandFor(entry) {
|
|
2402
|
+
const parts = ["node", SHIM, entry.hook];
|
|
2403
|
+
if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
|
|
2404
|
+
if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
|
|
2405
|
+
for (const arg of entry.args ?? []) parts.push(arg);
|
|
2406
|
+
return parts.join(" ");
|
|
2407
|
+
}
|
|
2408
|
+
function commandForPlugin(entry) {
|
|
2409
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
|
|
2410
|
+
}
|
|
2411
|
+
var CUSTOMER_LAUNCHER = ".cursor/heyspex/run-hook.mjs";
|
|
2412
|
+
function commandForCustomer(entry) {
|
|
2413
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
|
|
2414
|
+
}
|
|
2415
|
+
function identityFor(entry) {
|
|
2416
|
+
return `${entry.hook}::${entry.tool ?? ""}::${(entry.args ?? []).join(" ")}`;
|
|
2417
|
+
}
|
|
2418
|
+
function timeoutFor(entry) {
|
|
2419
|
+
return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// scripts/lib/customer-kit.mjs
|
|
2423
|
+
import { createHash } from "node:crypto";
|
|
2424
|
+
import fs from "node:fs";
|
|
2425
|
+
import path2 from "node:path";
|
|
2426
|
+
import os2 from "node:os";
|
|
2427
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
2428
|
+
|
|
2429
|
+
// scripts/lib/hook-dispositions.mjs
|
|
2430
|
+
var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
|
|
2431
|
+
var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
|
|
2432
|
+
var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
|
|
2433
|
+
var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
|
|
2434
|
+
var unportable = (control) => Object.freeze({ disposition: "unportable", control });
|
|
2435
|
+
var notShipped = Object.freeze({ disposition: "not-shipped" });
|
|
2436
|
+
var CC = mapped();
|
|
2437
|
+
var COPILOT_PROMPT_DROPPED = "Copilot drops command-hook output on userPromptSubmitted (no prompt-time context channel); the text moves to sessionStart / .github/copilot-instructions.md.";
|
|
2438
|
+
var CURSOR_PROMPT_USER_MESSAGE = "Cursor beforeSubmitPrompt honors user_message only (no additional_context): the text surfaces in the transcript instead of as invisible context.";
|
|
2439
|
+
var COPILOT_NO_PLAN_TOOL = "MCP preflight refusals (heyspex_claim_spec / heyspex_submit_for_review / heyspex_save_research) + .github/copilot-instructions.md plan rule (state the plan in chat, claim the spec before editing) + HS-20SW directive-obligation-gate claim-before-build deny";
|
|
2440
|
+
var HOOK_DISPOSITIONS = Object.freeze({
|
|
2441
|
+
// ── customer: session start / compaction ──────────────────────────────────
|
|
2442
|
+
"wip-checkpoint": {
|
|
2443
|
+
"claude-code": CC,
|
|
2444
|
+
cursor: mapped("preCompact is relayed as SessionStart(compact)."),
|
|
2445
|
+
copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
|
|
2446
|
+
},
|
|
2447
|
+
"inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2448
|
+
"check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2449
|
+
"check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2450
|
+
"git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
|
|
2451
|
+
"vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
|
|
2452
|
+
// ── customer: every user turn ─────────────────────────────────────────────
|
|
2453
|
+
"preflight-reminder": {
|
|
2454
|
+
"claude-code": CC,
|
|
2455
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2456
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
|
|
2457
|
+
},
|
|
2458
|
+
"intent-classifier-trigger": {
|
|
2459
|
+
"claude-code": CC,
|
|
2460
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2461
|
+
copilot: degraded(
|
|
2462
|
+
`${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
|
|
2463
|
+
)
|
|
2464
|
+
},
|
|
2465
|
+
"inject-communication-style": {
|
|
2466
|
+
"claude-code": CC,
|
|
2467
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2468
|
+
copilot: degraded(COPILOT_PROMPT_DROPPED)
|
|
2469
|
+
},
|
|
2470
|
+
"inject-vault-context": {
|
|
2471
|
+
"claude-code": CC,
|
|
2472
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2473
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
|
|
2474
|
+
},
|
|
2475
|
+
// ── customer: before a tool call (hard gates) ─────────────────────────────
|
|
2476
|
+
"git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
|
|
2477
|
+
"vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
|
|
2478
|
+
"settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
|
|
2479
|
+
"emit-claim-status": { "claude-code": CC, cursor: mapped(), copilot: mapped("Telemetry on preToolUse/postToolUse/agentStop; the cloud agent needs a network allow rule for the console.") },
|
|
2480
|
+
"enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
2481
|
+
"guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
2482
|
+
"directive-obligation-gate": {
|
|
2483
|
+
"claude-code": CC,
|
|
2484
|
+
cursor: mapped(),
|
|
2485
|
+
copilot: mapped("HS-20SW: hard deny on task/create/edit/write-shaped shell while a capture obligation is unmet; the only hard capture-on-idea guarantee on Copilot.")
|
|
2486
|
+
},
|
|
2487
|
+
"claim-before-code-gate": {
|
|
2488
|
+
"claude-code": CC,
|
|
2489
|
+
cursor: degraded("Armed only by plan-exit-ingest-gate, which has no Cursor trigger, so it stays dormant; covered by the HS-20SW claim-before-build rule."),
|
|
2490
|
+
copilot: degraded("Armed only by plan-exit-ingest-gate, which has no Copilot trigger (no ExitPlanMode tool), so it stays dormant; covered by the HS-20SW claim-before-build rule.")
|
|
2491
|
+
},
|
|
2492
|
+
"capture-workflow-gate": {
|
|
2493
|
+
"claude-code": CC,
|
|
2494
|
+
cursor: mapped(),
|
|
2495
|
+
copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
|
|
2496
|
+
},
|
|
2497
|
+
"diagnose-capture-reminder": {
|
|
2498
|
+
"claude-code": CC,
|
|
2499
|
+
cursor: mapped("preToolUse agent_message delivers the reminder."),
|
|
2500
|
+
copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
|
|
2501
|
+
},
|
|
2502
|
+
"git-stash-gate": {
|
|
2503
|
+
"claude-code": CC,
|
|
2504
|
+
cursor: mapped(),
|
|
2505
|
+
copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
|
|
2506
|
+
},
|
|
2507
|
+
"research-protocol-gate": { "claude-code": CC, cursor: mapped("Always on."), copilot: mapped("preToolUse on web_fetch/web_search and the MCP save_research tool. Always on; the MCP refusal duplicates it.") },
|
|
2508
|
+
"research-method-guard": {
|
|
2509
|
+
"claude-code": CC,
|
|
2510
|
+
cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
|
|
2511
|
+
copilot: unportable("MCP preflight refusal on heyspex_save_research (hard floor, hook-independent) + .github/copilot-instructions.md research rule; Copilot has no Skill or Workflow tool to hook.")
|
|
2512
|
+
},
|
|
2513
|
+
"deep-research-cost-gate": {
|
|
2514
|
+
"claude-code": CC,
|
|
2515
|
+
cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
|
|
2516
|
+
copilot: unportable("MCP preflight refusal on heyspex_save_research + .github/copilot-instructions.md cost-confirmation rule; Copilot has no Skill or Workflow tool to hook.")
|
|
2517
|
+
},
|
|
2518
|
+
"plan-review-gate": {
|
|
2519
|
+
"claude-code": CC,
|
|
2520
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
|
|
2521
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
2522
|
+
},
|
|
2523
|
+
"plan-exit-ingest-gate": {
|
|
2524
|
+
"claude-code": CC,
|
|
2525
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
|
|
2526
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
2527
|
+
},
|
|
2528
|
+
// ── customer: end of turn / session ───────────────────────────────────────
|
|
2529
|
+
"naughty-list-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped('agentStop {decision:"block"} re-drive, capped at 8 consecutive blocks.') },
|
|
2530
|
+
"id-display-format-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
|
|
2531
|
+
"epic-milestone-phrasing-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
|
|
2532
|
+
"enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
|
|
2533
|
+
"notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
2534
|
+
"close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
|
|
2535
|
+
"scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
2536
|
+
"session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
|
|
2537
|
+
// ── platform: heyspex-app development machinery (never installed in a customer repo) ──
|
|
2538
|
+
"inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2539
|
+
"inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2540
|
+
"coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2541
|
+
"coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2542
|
+
"agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
|
|
2543
|
+
"capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
|
|
2544
|
+
"check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2545
|
+
"dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2546
|
+
"env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
|
|
2547
|
+
"log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
|
|
2548
|
+
});
|
|
2549
|
+
function dispositionFor(hook, host) {
|
|
2550
|
+
return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// scripts/lib/customer-kit.mjs
|
|
2554
|
+
var HOOK_SCOPES = Object.freeze({
|
|
2555
|
+
// ── customer: vault / workflow / safety gates ─────────────────────────────
|
|
2556
|
+
"wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
|
|
2557
|
+
"inject-doctrine": {
|
|
2558
|
+
scope: "customer",
|
|
2559
|
+
why: "Owner ruling 2026-10-03 (HS-A2PO): ships. A customer session gets the baseline edition of SpexCode Doctrine (CUSTOMER_HOOK_DATA), the foundation its own learned layer is built on; never the platform's learned file."
|
|
2560
|
+
},
|
|
2561
|
+
"check-auto-memory-budget": { scope: "customer", why: "Auto-memory index budget for the session project." },
|
|
2562
|
+
"check-claude-md-budget": { scope: "customer", why: "Budget check on the project's always-loaded agent contract." },
|
|
2563
|
+
"git-reflog-audit": { scope: "customer", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
|
|
2564
|
+
"preflight-reminder": { scope: "customer", why: "Names the required procedure before filing/destructive turns." },
|
|
2565
|
+
"intent-classifier-trigger": { scope: "customer", why: "Routes capture/research/settings/defer intents." },
|
|
2566
|
+
"inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
|
|
2567
|
+
"inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
|
|
2568
|
+
"git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
|
|
2569
|
+
"vault-write-tier-guard": { scope: "customer", why: "Cloud write mode: disk edits to vault specs are a mirror the console never reads back (HS-FCNH)." },
|
|
2570
|
+
"settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
|
|
2571
|
+
"emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
|
|
2572
|
+
"enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
|
|
2573
|
+
"guard-auto-memory-writes": { scope: "customer", why: "No new topic files in the auto-memory directory." },
|
|
2574
|
+
"plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
|
|
2575
|
+
"plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
|
|
2576
|
+
"claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
|
|
2577
|
+
"directive-obligation-gate": { scope: "customer", why: "A capture_on_idea directive leaves a durable obligation; an unmet one denies sub-agent spawns, product-code writes and builds, and re-drives Stop once. Also claim-before-build (HS-20SW)." },
|
|
2578
|
+
"capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
|
|
2579
|
+
"vault-mirror-catchup": { scope: "customer", why: "Cloud write mode: a SessionStart detached `hs pull --apply` closes a stale disk mirror before the first tool call (HS-XJFQ)." },
|
|
2580
|
+
"diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
|
|
2581
|
+
"git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
|
|
2582
|
+
"research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
|
|
2583
|
+
"deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
|
|
2584
|
+
"research-protocol-gate": { scope: "customer", why: "WebSearch/WebFetch refused until research preflight ran (HS-K0ZP, HS-EN4F)." },
|
|
2585
|
+
"naughty-list-gate": { scope: "customer", why: "Re-drive a final message that uses banned jargon." },
|
|
2586
|
+
"id-display-format-gate": { scope: "customer", why: "Naming V3 display-id format in replies." },
|
|
2587
|
+
"epic-milestone-phrasing-gate": { scope: "customer", why: "Epic/milestone phrasing rules in replies." },
|
|
2588
|
+
"enforce-session-sequence": { scope: "customer", why: "Re-drive a turn that shipped without record_wip." },
|
|
2589
|
+
"notify-status-flip": { scope: "customer", why: "Sound on an in_review status flip." },
|
|
2590
|
+
"close-terminal-on-ship": { scope: "customer", why: "Close the launch terminal once the spec ships." },
|
|
2591
|
+
"scripts/stamp-provenance-on-edit.mjs": { scope: "customer", why: "Stamp assisted_by provenance on direct spec edits (active vault)." },
|
|
2592
|
+
"session-end": { scope: "customer", why: "Release the claim and export session events." },
|
|
2593
|
+
// ── platform: heyspex-app's own development machinery ─────────────────────
|
|
2594
|
+
"inject-dev-server-awareness": { scope: "platform", why: "About the spex-console dev server in heyspex-app." },
|
|
2595
|
+
"inject-coo-seat": { scope: "platform", why: "dev-factory COO seat mandate." },
|
|
2596
|
+
"coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
|
|
2597
|
+
"coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
|
|
2598
|
+
"agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
|
|
2599
|
+
"capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
|
|
2600
|
+
"check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
|
|
2601
|
+
"dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
|
|
2602
|
+
"env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
|
|
2603
|
+
"log-session-activity": { scope: "platform", why: "Resolves .heyspex state from the platform checkout's own location, not the project \u2014 would log customer sessions into heyspex-app." }
|
|
2604
|
+
});
|
|
2605
|
+
var CUSTOMER_HOOK_DATA = Object.freeze({
|
|
2606
|
+
"inject-doctrine": Object.freeze(["spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json"])
|
|
2607
|
+
});
|
|
2608
|
+
var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
2609
|
+
{ event: "SessionStart", hook: "wip-checkpoint", timeout: 5 },
|
|
2610
|
+
{ event: "SessionStart", hook: "inject-dev-server-awareness", timeout: 5 },
|
|
2611
|
+
{ event: "SessionStart", hook: "inject-coo-seat", timeout: 5 },
|
|
2612
|
+
{ event: "SessionStart", hook: "inject-doctrine", timeout: 5 },
|
|
2613
|
+
{ event: "SessionStart", hook: "coo-guard-check", timeout: 5 },
|
|
2614
|
+
{ event: "SessionStart", hook: "check-auto-memory-budget", timeout: 5 },
|
|
2615
|
+
{ event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
|
|
2616
|
+
{ event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
|
|
2617
|
+
{ event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
|
|
2618
|
+
{ event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
|
|
2619
|
+
{ event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
|
|
2620
|
+
{ event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
|
|
2621
|
+
{ event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
|
|
2622
|
+
{ event: "UserPromptSubmit", hook: "inject-communication-style", timeout: 5 },
|
|
2623
|
+
{ event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
|
|
2624
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
|
|
2625
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
|
|
2626
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
|
|
2627
|
+
{ event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
|
|
2628
|
+
{ event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
|
|
2629
|
+
{ event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
|
|
2630
|
+
{ event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
|
|
2631
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
|
|
2632
|
+
{ event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
|
|
2633
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
|
|
2634
|
+
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
|
|
2635
|
+
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
|
|
2636
|
+
{ event: "PreToolUse", matcher: "Edit|MultiEdit|Write|Bash|PowerShell", hook: "preflight-reminder", timeout: 5 },
|
|
2637
|
+
{ event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-review-gate", timeout: 8 },
|
|
2638
|
+
{ event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
|
|
2639
|
+
{ event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
|
|
2640
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
|
|
2641
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
|
|
2642
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell|mcp__neon__prepare_database_migration|mcp__neon__complete_database_migration|mcp__neon__reset_from_parent|mcp__neon__delete_branch|mcp__Neon__prepare_database_migration|mcp__Neon__complete_database_migration|mcp__Neon__reset_from_parent|mcp__Neon__delete_branch", hook: "dev-server-task-reminder", timeout: 10 },
|
|
2643
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
|
|
2644
|
+
{ event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
|
|
2645
|
+
{ event: "PreToolUse", matcher: "Workflow|Skill", hook: "deep-research-cost-gate", timeout: 5 },
|
|
2646
|
+
{ event: "PreToolUse", matcher: "WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|mcp__plugin_heyspex_heyspex__heyspex_save_research", hook: "research-protocol-gate", timeout: 5 },
|
|
2647
|
+
{ event: "PreToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
|
|
2648
|
+
{ event: "Stop", hook: "emit-claim-status", args: ["--event=Stop"], timeout: 5 },
|
|
2649
|
+
{ event: "Stop", hook: "naughty-list-gate", timeout: 5 },
|
|
2650
|
+
{ event: "Stop", hook: "id-display-format-gate", timeout: 5 },
|
|
2651
|
+
{ event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
|
|
2652
|
+
{ event: "Stop", hook: "git-reflog-audit", timeout: 5 },
|
|
2653
|
+
{ event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
|
|
2654
|
+
{ event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
|
|
2655
|
+
{ event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
|
|
2656
|
+
{ event: "PostToolUse", matcher: "Skill|mcp__heyspex__heyspex_update_capture|mcp__heyspex__heyspex_promote_capture.*|mcp__heyspex__heyspex_file_spec|mcp__heyspex__heyspex_claim_spec|mcp__heyspex__heyspex_release_spec|mcp__heyspex__heyspex_defer_spec", hook: "directive-obligation-gate", timeout: 5 },
|
|
2657
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
|
|
2658
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
|
|
2659
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
|
|
2660
|
+
{ event: "PostToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
|
|
2661
|
+
{ event: "SessionEnd", hook: "session-end", timeout: 5 },
|
|
2662
|
+
{ event: "SessionEnd", hook: "close-terminal-on-ship", timeout: 5 }
|
|
2663
|
+
]);
|
|
2664
|
+
var COMMAND_SCOPES = Object.freeze({
|
|
2665
|
+
"hs-backfill.md": { scope: "customer", why: "Backfill completed work into the vault." },
|
|
2666
|
+
"hs-capture.md": { scope: "customer", why: "Capture intake." },
|
|
2667
|
+
"hs-defer.md": { scope: "customer", why: "Split + defer remaining work." },
|
|
2668
|
+
"hs-doctor.md": { scope: "customer", why: "Install-surface repair when heyspex_* tools are missing." },
|
|
2669
|
+
"hs-ground.md": { scope: "customer", why: "Pre-plan grounding report." },
|
|
2670
|
+
"hs-handoff.md": { scope: "customer", why: "Short-code lane handoff." },
|
|
2671
|
+
"hs-next.md": { scope: "customer", why: "On-deck prompt + claim handshake." },
|
|
2672
|
+
"hs-promote.md": { scope: "customer", why: "Capture \u2192 research \u2192 milestone promotion." },
|
|
2673
|
+
"hs-push.md": { scope: "customer", why: "Push an epic plan into the vault." },
|
|
2674
|
+
"hs-qc.md": { scope: "customer", why: "Post-plan QC gate." },
|
|
2675
|
+
"hs-res.md": { scope: "customer", why: "Research method entrypoint (SpexCrawl/DeepSeek)." },
|
|
2676
|
+
"hs-resolve.md": { scope: "customer", why: "Resolution lane." },
|
|
2677
|
+
"hs-review.md": { scope: "customer", why: "Review queue." },
|
|
2678
|
+
"hs-view.md": { scope: "customer", why: "Open a console surface." },
|
|
2679
|
+
"hs-auto.md": { scope: "platform", why: "Routes work into dev-factory (platform build system)." },
|
|
2680
|
+
"hs-dev.md": { scope: "platform", why: "Admin/dev-only HeySpex dev/prod mode switch." },
|
|
2681
|
+
"hs-log.md": { scope: "platform", why: "Admin/dev-only dev-server log watcher." },
|
|
2682
|
+
"kill-ports.md": { scope: "platform", why: "Kills local dev-server ports." },
|
|
2683
|
+
"prototype.md": { scope: "platform", why: "Console design-preview prototypes inside spex-console." }
|
|
2684
|
+
});
|
|
2685
|
+
function hookScope(hook) {
|
|
2686
|
+
return HOOK_SCOPES[hook]?.scope ?? null;
|
|
2687
|
+
}
|
|
2688
|
+
function toPosixPath(p) {
|
|
2689
|
+
return String(p).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2690
|
+
}
|
|
2691
|
+
function samePlatformPath(a, b) {
|
|
2692
|
+
if (!a || !b) return false;
|
|
2693
|
+
const norm = (p) => {
|
|
2694
|
+
const s = toPosixPath(path2.resolve(p));
|
|
2695
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
2696
|
+
};
|
|
2697
|
+
return norm(a) === norm(b);
|
|
2698
|
+
}
|
|
2699
|
+
var MCP_PROJECT_PREFIX = "mcp__heyspex__";
|
|
2700
|
+
var MCP_PLUGIN_PREFIX = "mcp__plugin_heyspex_heyspex__";
|
|
2701
|
+
function widenMcpMatcher(matcher) {
|
|
2702
|
+
if (!matcher) return matcher;
|
|
2703
|
+
const parts = matcher.split("|");
|
|
2704
|
+
const out = [];
|
|
2705
|
+
for (const part of parts) {
|
|
2706
|
+
if (!out.includes(part)) out.push(part);
|
|
2707
|
+
if (part.startsWith(MCP_PROJECT_PREFIX)) {
|
|
2708
|
+
const plugin = MCP_PLUGIN_PREFIX + part.slice(MCP_PROJECT_PREFIX.length);
|
|
2709
|
+
if (!parts.includes(plugin) && !out.includes(plugin)) out.push(plugin);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return out.join("|");
|
|
2713
|
+
}
|
|
2714
|
+
function customerHookRows() {
|
|
2715
|
+
return CLAUDE_HOOK_ROWS.filter((r) => hookScope(r.hook) === "customer");
|
|
2716
|
+
}
|
|
2717
|
+
function hookTail(row) {
|
|
2718
|
+
return [row.hook, ...row.args ?? []].join(" ");
|
|
2719
|
+
}
|
|
2720
|
+
var CLAUDE_HOOK_SHIM = "scripts/claude-hooks/_run.mjs";
|
|
2721
|
+
var CURSOR_HOOK_SHIM = "scripts/cursor-hooks/_run.mjs";
|
|
2722
|
+
var CUSTOMER_LAUNCHERS = Object.freeze({
|
|
2723
|
+
claude: Object.freeze({
|
|
2724
|
+
rel: Object.freeze([".claude", "heyspex", "run-hook.mjs"]),
|
|
2725
|
+
projectDirEnv: "CLAUDE_PROJECT_DIR",
|
|
2726
|
+
shim: CLAUDE_HOOK_SHIM,
|
|
2727
|
+
registry: ".claude/settings.json"
|
|
2728
|
+
}),
|
|
2729
|
+
cursor: Object.freeze({
|
|
2730
|
+
rel: Object.freeze([".cursor", "heyspex", "run-hook.mjs"]),
|
|
2731
|
+
projectDirEnv: "CURSOR_PROJECT_DIR",
|
|
2732
|
+
shim: CURSOR_HOOK_SHIM,
|
|
2733
|
+
registry: ".cursor/hooks.json"
|
|
2734
|
+
})
|
|
2735
|
+
});
|
|
2736
|
+
var BOOTSTRAP_WALK = "var d=process.cwd(),r=e.CLAUDE_PROJECT_DIR;if(!r||!f.existsSync(p.join(r,...n))){r='';for(;;){if(f.existsSync(p.join(d,...n))){r=d;break}var q=p.dirname(d);if(q===d)break;d=q}}";
|
|
2737
|
+
function buildBootstrap({ plugin = false } = {}) {
|
|
2738
|
+
const walk = BOOTSTRAP_WALK;
|
|
2739
|
+
const body = plugin ? "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='',m=e.CLAUDE_PLUGIN_ROOT||e.COPILOT_PLUGIN_ROOT||e.PLUGIN_ROOT;if(m&&f.existsSync(p.join(m,'bin','run-hook.mjs')))s=p.join(m,'bin','run-hook.mjs');if(!s){" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}}" : "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='';" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}";
|
|
2740
|
+
return body + "if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" + (plugin ? "//bin/run-hook.mjs" : "//scripts/claude-hooks/_run.mjs");
|
|
2741
|
+
}
|
|
2742
|
+
var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
|
|
2743
|
+
var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
|
|
2744
|
+
function buildCustomerBootstrap() {
|
|
2745
|
+
const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
|
|
2746
|
+
return "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=[" + rel + "],s='';" + BOOTSTRAP_WALK + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});//" + CUSTOMER_LAUNCHERS.claude.rel.join("/");
|
|
2747
|
+
}
|
|
2748
|
+
var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
|
|
2749
|
+
var LAUNCHER_TEMPLATE = [
|
|
2750
|
+
"#!/usr/bin/env node",
|
|
2751
|
+
"// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
|
|
2752
|
+
"//",
|
|
2753
|
+
"// Every HeySpex hook registered in __REGISTRY__ runs",
|
|
2754
|
+
'// node "$__ENV__/__LAUNCHER__" <hook> [args]',
|
|
2755
|
+
"// This file carries NO machine-specific path. It finds the heyspex package this",
|
|
2756
|
+
"// repo depends on (node_modules/heyspex) and hands off in-process to the",
|
|
2757
|
+
"// package's hook shim (__SHIM__), so the same committed command works on every",
|
|
2758
|
+
"// machine the repo is cloned to.",
|
|
2759
|
+
"//",
|
|
2760
|
+
"// Resolution order (the first root that carries the shim wins):",
|
|
2761
|
+
"// 1. HEYSPEX_PLATFORM_ROOT explicit machine-local override",
|
|
2762
|
+
"// 2. HEYSPEX_MCP_ENTRY -> its package root machine-local override",
|
|
2763
|
+
"// 3. node_modules/heyspex, walking up from this file (the repo-local dependency)",
|
|
2764
|
+
"// 4. node_modules/heyspex up from $__ENV__, $CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR, cwd",
|
|
2765
|
+
"// 5. <npm root -g>/heyspex a global install, last resort",
|
|
2766
|
+
"// Nothing found -> one stderr line and exit 0 (fail-open, like the shim itself).",
|
|
2767
|
+
"import { spawnSync } from 'node:child_process';",
|
|
2768
|
+
"import { existsSync } from 'node:fs';",
|
|
2769
|
+
"import { dirname, resolve } from 'node:path';",
|
|
2770
|
+
"import { fileURLToPath, pathToFileURL } from 'node:url';",
|
|
2771
|
+
"",
|
|
2772
|
+
"const SHIM = '__SHIM__'.split('/');",
|
|
2773
|
+
"const here = dirname(fileURLToPath(import.meta.url));",
|
|
2774
|
+
"const env = process.env;",
|
|
2775
|
+
"const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);",
|
|
2776
|
+
"const hasShim = (root) => Boolean(root) && existsSync(resolve(root, ...SHIM));",
|
|
2777
|
+
"",
|
|
2778
|
+
"function packageUpFrom(start) {",
|
|
2779
|
+
" let dir = resolve(start);",
|
|
2780
|
+
" for (;;) {",
|
|
2781
|
+
" const candidate = resolve(dir, 'node_modules', 'heyspex');",
|
|
2782
|
+
" if (hasShim(candidate)) return candidate;",
|
|
2783
|
+
" const parent = dirname(dir);",
|
|
2784
|
+
" if (parent === dir) return null;",
|
|
2785
|
+
" dir = parent;",
|
|
2786
|
+
" }",
|
|
2787
|
+
"}",
|
|
2788
|
+
"",
|
|
2789
|
+
"function globalPackage() {",
|
|
2790
|
+
" try {",
|
|
2791
|
+
" // One command string through the shell: npm is a .cmd on Windows, and a",
|
|
2792
|
+
" // shell + args array is what node deprecates (DEP0190).",
|
|
2793
|
+
" const res = spawnSync('npm root -g', { encoding: 'utf8', timeout: 5000, shell: true });",
|
|
2794
|
+
" const root = String(res.stdout ?? '').trim();",
|
|
2795
|
+
" return root ? resolve(root, 'heyspex') : null;",
|
|
2796
|
+
" } catch {",
|
|
2797
|
+
" return null;",
|
|
2798
|
+
" }",
|
|
2799
|
+
"}",
|
|
2800
|
+
"",
|
|
2801
|
+
"const mcpEntry = text(env.HEYSPEX_MCP_ENTRY);",
|
|
2802
|
+
"const starts = [...new Set([here, text(env.__ENV__), text(env.CLAUDE_PROJECT_DIR), text(env.CURSOR_PROJECT_DIR), process.cwd()].filter(Boolean))];",
|
|
2803
|
+
"let root =",
|
|
2804
|
+
" [text(env.HEYSPEX_PLATFORM_ROOT), mcpEntry && resolve(mcpEntry, '..', '..', '..')].find(hasShim) ??",
|
|
2805
|
+
" starts.map(packageUpFrom).find(Boolean) ??",
|
|
2806
|
+
" null;",
|
|
2807
|
+
"if (!root) {",
|
|
2808
|
+
" const global = globalPackage();",
|
|
2809
|
+
" if (hasShim(global)) root = global;",
|
|
2810
|
+
"}",
|
|
2811
|
+
"",
|
|
2812
|
+
"if (!root) {",
|
|
2813
|
+
" process.stderr.write(",
|
|
2814
|
+
" '[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: no heyspex package found ' +",
|
|
2815
|
+
` '(run "npm install heyspex" in this repo, or set HEYSPEX_PLATFORM_ROOT). Looked for __SHIM__ under ' +`,
|
|
2816
|
+
" 'HEYSPEX_PLATFORM_ROOT, HEYSPEX_MCP_ENTRY, node_modules/heyspex up from ' + starts.join(', ') + ', and npm root -g.\\n',",
|
|
2817
|
+
" );",
|
|
2818
|
+
" process.exit(0);",
|
|
2819
|
+
"}",
|
|
2820
|
+
"",
|
|
2821
|
+
"// argv is already [node, <this file>, <hook>, ...args] -- the shape the shim",
|
|
2822
|
+
'// reads; the shim path replaces this file so a shim guarded by "am I main?"',
|
|
2823
|
+
"// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
|
|
2824
|
+
"const shim = resolve(root, ...SHIM);",
|
|
2825
|
+
"process.argv[1] = shim;",
|
|
2826
|
+
"try {",
|
|
2827
|
+
" await import(pathToFileURL(shim).href);",
|
|
2828
|
+
"} catch (err) {",
|
|
2829
|
+
" process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
|
|
2830
|
+
" process.exit(0);",
|
|
2831
|
+
"}",
|
|
2832
|
+
""
|
|
2833
|
+
].join("\n");
|
|
2834
|
+
function renderCustomerLauncher(surface = "claude") {
|
|
2835
|
+
const l = CUSTOMER_LAUNCHERS[surface];
|
|
2836
|
+
return LAUNCHER_TEMPLATE.replace(
|
|
2837
|
+
/__(REGISTRY|ENV|LAUNCHER|SHIM)__/g,
|
|
2838
|
+
(_m, token) => token === "REGISTRY" ? l.registry : token === "ENV" ? l.projectDirEnv : token === "LAUNCHER" ? l.rel.join("/") : l.shim
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
function planCustomerLauncherWrite(existing, surface = "claude") {
|
|
2842
|
+
const text = renderCustomerLauncher(surface);
|
|
2843
|
+
if (existing === null || existing === void 0) return { write: true, reason: "missing", text };
|
|
2844
|
+
if (String(existing).replace(/\r\n/g, "\n") === text) return { write: false, reason: "unchanged", text };
|
|
2845
|
+
return { write: true, reason: "update", text };
|
|
2846
|
+
}
|
|
2847
|
+
function renderCustomerHooks(runnerPrefix) {
|
|
2848
|
+
return customerHookRows().map((row) => ({
|
|
2849
|
+
event: row.event,
|
|
2850
|
+
...row.matcher ? { matcher: widenMcpMatcher(row.matcher) } : {},
|
|
2851
|
+
command: `${runnerPrefix} ${hookTail(row)}`,
|
|
2852
|
+
timeout: row.timeout,
|
|
2853
|
+
tail: hookTail(row)
|
|
2854
|
+
}));
|
|
2855
|
+
}
|
|
2856
|
+
function renderPluginHooksDocument() {
|
|
2857
|
+
const hooks = {};
|
|
2858
|
+
for (const d of renderCustomerHooks(PLUGIN_RUNNER_PREFIX)) {
|
|
2859
|
+
(hooks[d.event] ??= []).push({
|
|
2860
|
+
...d.matcher ? { matcher: d.matcher } : {},
|
|
2861
|
+
hooks: [{ type: "command", command: d.command, timeout: d.timeout }]
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
return {
|
|
2865
|
+
hooks
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function renderPluginHooksText() {
|
|
2869
|
+
return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
|
|
2870
|
+
`;
|
|
2871
|
+
}
|
|
2872
|
+
var COPILOT_EVENT_NAMES = Object.freeze({
|
|
2873
|
+
SessionStart: "sessionStart",
|
|
2874
|
+
SessionEnd: "sessionEnd",
|
|
2875
|
+
UserPromptSubmit: "userPromptSubmitted",
|
|
2876
|
+
PreToolUse: "preToolUse",
|
|
2877
|
+
PostToolUse: "postToolUse",
|
|
2878
|
+
Stop: "agentStop",
|
|
2879
|
+
PreCompact: "preCompact"
|
|
2880
|
+
});
|
|
2881
|
+
var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
|
|
2882
|
+
var COPILOT_TOOL_NAMES = Object.freeze({
|
|
2883
|
+
Write: "create",
|
|
2884
|
+
Edit: "edit",
|
|
2885
|
+
MultiEdit: "edit",
|
|
2886
|
+
NotebookEdit: "edit",
|
|
2887
|
+
Bash: "bash",
|
|
2888
|
+
PowerShell: "powershell",
|
|
2889
|
+
Agent: "task",
|
|
2890
|
+
Task: "task",
|
|
2891
|
+
WebFetch: "web_fetch",
|
|
2892
|
+
WebSearch: "web_search"
|
|
2893
|
+
});
|
|
2894
|
+
function toCopilotMatcher(matcher) {
|
|
2895
|
+
if (!matcher) return void 0;
|
|
2896
|
+
const out = [];
|
|
2897
|
+
for (const part of matcher.split("|")) {
|
|
2898
|
+
let t = null;
|
|
2899
|
+
if (part === ".*") t = ".*";
|
|
2900
|
+
else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
|
|
2901
|
+
else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
|
|
2902
|
+
else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
|
|
2903
|
+
if (t && !out.includes(t)) out.push(t);
|
|
2904
|
+
}
|
|
2905
|
+
if (out.includes(".*")) return ".*";
|
|
2906
|
+
return out.length ? out.join("|") : null;
|
|
2907
|
+
}
|
|
2908
|
+
function githubHookNames() {
|
|
2909
|
+
const names = /* @__PURE__ */ new Set();
|
|
2910
|
+
for (const r of customerHookRows()) {
|
|
2911
|
+
if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
|
|
2912
|
+
}
|
|
2913
|
+
return names;
|
|
2914
|
+
}
|
|
2915
|
+
function githubHookCommand(row) {
|
|
2916
|
+
const tail = hookTail(row);
|
|
2917
|
+
return `R=node_modules/heyspex/scripts/claude-hooks/_run.mjs; [ -f "$R" ] || R=./scripts/claude-hooks/_run.mjs; [ -f "$R" ] || exit 0; exec node "$R" ${tail}`;
|
|
2918
|
+
}
|
|
2919
|
+
function renderGithubHooksDocument() {
|
|
2920
|
+
const allowed = githubHookNames();
|
|
2921
|
+
const hooks = {};
|
|
2922
|
+
for (const row of customerHookRows()) {
|
|
2923
|
+
if (!allowed.has(row.hook)) continue;
|
|
2924
|
+
const event = COPILOT_EVENT_NAMES[row.event];
|
|
2925
|
+
if (!event) continue;
|
|
2926
|
+
const matcher = toCopilotMatcher(row.matcher);
|
|
2927
|
+
if (matcher === null) continue;
|
|
2928
|
+
(hooks[event] ??= []).push({
|
|
2929
|
+
type: "command",
|
|
2930
|
+
...matcher ? { matcher } : {},
|
|
2931
|
+
bash: githubHookCommand(row),
|
|
2932
|
+
timeoutSec: row.timeout
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
return { version: 1, hooks };
|
|
2936
|
+
}
|
|
2937
|
+
function renderGithubHooksText() {
|
|
2938
|
+
return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
|
|
2939
|
+
`;
|
|
2940
|
+
}
|
|
2941
|
+
var HARNESS_COMMAND_SURFACES = Object.freeze([
|
|
2942
|
+
{
|
|
2943
|
+
installedDir: Object.freeze([".claude", "commands"]),
|
|
2944
|
+
packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "claude"]),
|
|
2945
|
+
devSourceDir: Object.freeze([".claude", "commands"])
|
|
2946
|
+
},
|
|
2947
|
+
{
|
|
2948
|
+
installedDir: Object.freeze([".cursor", "commands"]),
|
|
2949
|
+
packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "cursor"]),
|
|
2950
|
+
devSourceDir: Object.freeze([".cursor", "commands"])
|
|
2951
|
+
}
|
|
2952
|
+
]);
|
|
2953
|
+
var NPM_ALIASES = Object.freeze({ doctor: "doctor", setup: "doctor --fix" });
|
|
2954
|
+
var SCRIPT_VERBS = Object.freeze({
|
|
2955
|
+
"scripts/heyspex-doctor.mjs": "doctor",
|
|
2956
|
+
"scripts/hs-view.mjs": "view",
|
|
2957
|
+
"scripts/hs-capture-cli.mjs": "capture",
|
|
2958
|
+
"scripts/hs-next.mjs": "next",
|
|
2959
|
+
"scripts/hs-mode.mjs": "mode",
|
|
2960
|
+
"scripts/hs-settings.mjs": "settings",
|
|
2961
|
+
"scripts/hs-companion.mjs": "companion",
|
|
2962
|
+
"scripts/wip-ledger-health.mjs": "wip",
|
|
2963
|
+
"scripts/dev-server-status.mjs": "server status",
|
|
2964
|
+
"scripts/dev-server-stop.mjs": "server stop"
|
|
2965
|
+
});
|
|
2966
|
+
var PORTABLE_PACKAGE_DIR = "node_modules/heyspex";
|
|
2967
|
+
var fsDeps = {
|
|
2968
|
+
joinPath: (...s) => path2.join(...s),
|
|
2969
|
+
readFile: (p) => {
|
|
2970
|
+
try {
|
|
2971
|
+
return fs.readFileSync(p, "utf8");
|
|
2972
|
+
} catch {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
},
|
|
2976
|
+
listDir: (d) => {
|
|
2977
|
+
try {
|
|
2978
|
+
return fs.readdirSync(d);
|
|
2979
|
+
} catch {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
function invokedDirectly(importMetaUrl, selfBasename, argv1 = process.argv[1]) {
|
|
2985
|
+
try {
|
|
2986
|
+
if (!argv1) return false;
|
|
2987
|
+
const self = fileURLToPath2(importMetaUrl);
|
|
2988
|
+
if (path2.basename(self) !== selfBasename) return false;
|
|
2989
|
+
return pathToFileURL(path2.resolve(argv1)).href === pathToFileURL(self).href;
|
|
2990
|
+
} catch {
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
var SELF = fileURLToPath2(import.meta.url);
|
|
2995
|
+
if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
|
|
2996
|
+
const platformRoot = path2.resolve(path2.dirname(SELF), "..", "..");
|
|
2997
|
+
const target = path2.join(platformRoot, "plugins", "heyspex", "hooks", "hooks.json");
|
|
2998
|
+
const want = renderPluginHooksText();
|
|
2999
|
+
const same = (fsDeps.readFile(target) ?? "").replace(/\r\n/g, "\n") === want;
|
|
3000
|
+
if (process.argv.includes("--write-plugin-hooks")) {
|
|
3001
|
+
if (!same) fs.writeFileSync(target, want, "utf8");
|
|
3002
|
+
console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path2.relative(platformRoot, target)}`);
|
|
3003
|
+
} else if (process.argv.includes("--check-plugin-hooks")) {
|
|
3004
|
+
if (!same) {
|
|
3005
|
+
console.error("customer-kit: plugins/heyspex/hooks/hooks.json is stale \u2014 run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`");
|
|
3006
|
+
process.exit(1);
|
|
3007
|
+
}
|
|
3008
|
+
console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
|
|
3009
|
+
} else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
|
|
3010
|
+
const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
|
|
3011
|
+
const ghWant = renderGithubHooksText();
|
|
3012
|
+
const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
|
|
3013
|
+
if (process.argv.includes("--write-github-hooks")) {
|
|
3014
|
+
if (!ghSame) {
|
|
3015
|
+
fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
|
|
3016
|
+
fs.writeFileSync(ghTarget, ghWant, "utf8");
|
|
3017
|
+
}
|
|
3018
|
+
console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
|
|
3019
|
+
} else if (!ghSame) {
|
|
3020
|
+
console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
|
|
3021
|
+
process.exit(1);
|
|
3022
|
+
} else {
|
|
3023
|
+
console.log("customer-kit: .github/hooks/heyspex.json up to date");
|
|
3024
|
+
}
|
|
3025
|
+
} else {
|
|
3026
|
+
console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// scripts/install-cursor-hooks.mjs
|
|
3031
|
+
var PLATFORM_ROOT = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3032
|
+
var REPO_ROOT = process.env.HS_SETUP_TARGET?.trim() ? path3.resolve(process.env.HS_SETUP_TARGET.trim()) : PLATFORM_ROOT;
|
|
3033
|
+
var SETTINGS_PATH = path3.join(REPO_ROOT, ".cursor", "hooks.json");
|
|
3034
|
+
function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
|
|
3035
|
+
const customer = !samePlatformPath(targetRoot, platformRoot);
|
|
3036
|
+
return {
|
|
3037
|
+
customer,
|
|
3038
|
+
hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === "customer") : HOOKS,
|
|
3039
|
+
commandFor: customer ? commandForCustomer : commandFor,
|
|
3040
|
+
launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
var KIT = cursorKitFor(REPO_ROOT);
|
|
3044
|
+
var ACTIVE_HOOKS = KIT.hooks;
|
|
3045
|
+
var LAUNCHER_PATH = path3.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
|
|
3046
|
+
var LEGACY_MARKER = "scripts/claude-hooks/";
|
|
3047
|
+
function readSettings() {
|
|
3048
|
+
try {
|
|
3049
|
+
const raw = fs2.readFileSync(SETTINGS_PATH, "utf8").trim();
|
|
3050
|
+
if (!raw) return { version: 1, hooks: {} };
|
|
3051
|
+
const parsed = JSON.parse(raw);
|
|
3052
|
+
if (parsed && typeof parsed === "object") {
|
|
3053
|
+
parsed.version ??= 1;
|
|
3054
|
+
parsed.hooks ??= {};
|
|
3055
|
+
return parsed;
|
|
3056
|
+
}
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
if (err && err.code === "ENOENT") return { version: 1, hooks: {} };
|
|
3059
|
+
throw err;
|
|
3060
|
+
}
|
|
3061
|
+
return { version: 1, hooks: {} };
|
|
3062
|
+
}
|
|
3063
|
+
function writeSettings(settings) {
|
|
3064
|
+
fs2.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
|
|
3065
|
+
fs2.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
|
|
3066
|
+
`, "utf8");
|
|
3067
|
+
}
|
|
3068
|
+
var commandOf = (entry) => entry && typeof entry.command === "string" ? entry.command : "";
|
|
3069
|
+
var isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
|
|
3070
|
+
var isShimEntry = (raw) => isOurRunner(commandOf(raw));
|
|
3071
|
+
var isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
|
|
3072
|
+
function identityOfCommand(command) {
|
|
3073
|
+
const tokens = command.trim().split(/\s+/);
|
|
3074
|
+
const shimAt = tokens.findIndex(isOurRunner);
|
|
3075
|
+
if (shimAt === -1) return null;
|
|
3076
|
+
const rest = tokens.slice(shimAt + 1);
|
|
3077
|
+
const hook = rest[0] ?? "";
|
|
3078
|
+
let tool = "";
|
|
3079
|
+
const args = [];
|
|
3080
|
+
for (const token of rest.slice(1)) {
|
|
3081
|
+
if (token.startsWith("--hs-tool=")) {
|
|
3082
|
+
tool = token.slice("--hs-tool=".length).replace(/^"([\s\S]*)"$/, "$1");
|
|
3083
|
+
} else if (token.startsWith("--hs-claude-event=")) continue;
|
|
3084
|
+
else args.push(token);
|
|
3085
|
+
}
|
|
3086
|
+
return `${hook}::${tool}::${args.join(" ")}`;
|
|
3087
|
+
}
|
|
3088
|
+
function desiredEntry(entry, render = KIT.commandFor) {
|
|
3089
|
+
return {
|
|
3090
|
+
command: render(entry),
|
|
3091
|
+
timeout: timeoutFor(entry),
|
|
3092
|
+
...entry.matcher ? { matcher: entry.matcher } : {}
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function sameEntry(raw, entry, render = KIT.commandFor) {
|
|
3096
|
+
const want = desiredEntry(entry, render);
|
|
3097
|
+
return commandOf(raw) === want.command && raw.timeout === want.timeout && (raw.matcher ?? void 0) === (want.matcher ?? void 0);
|
|
3098
|
+
}
|
|
3099
|
+
var PLUGIN_HOOKS_PATH = path3.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
|
|
3100
|
+
function pluginHooksDocument() {
|
|
3101
|
+
const hooks = {};
|
|
3102
|
+
for (const entry of HOOKS) {
|
|
3103
|
+
hooks[entry.event] ??= [];
|
|
3104
|
+
hooks[entry.event].push({
|
|
3105
|
+
command: commandForPlugin(entry),
|
|
3106
|
+
timeout: timeoutFor(entry),
|
|
3107
|
+
...entry.matcher ? { matcher: entry.matcher } : {}
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
return {
|
|
3111
|
+
_comment: "GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs \u2014 do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through $CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json still registers the same hooks runs every hook twice \u2014 validate in a fresh clone, or run `node scripts/install-cursor-hooks.mjs --uninstall` there first.",
|
|
3112
|
+
version: 1,
|
|
3113
|
+
hooks
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function pluginHooksText() {
|
|
3117
|
+
return `${JSON.stringify(pluginHooksDocument(), null, 2)}
|
|
3118
|
+
`;
|
|
3119
|
+
}
|
|
3120
|
+
function syncPluginHooks() {
|
|
3121
|
+
const want = pluginHooksText();
|
|
3122
|
+
let have = null;
|
|
3123
|
+
try {
|
|
3124
|
+
have = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
|
|
3125
|
+
} catch {
|
|
3126
|
+
have = null;
|
|
3127
|
+
}
|
|
3128
|
+
if (have === want) return false;
|
|
3129
|
+
fs2.mkdirSync(path3.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
|
|
3130
|
+
fs2.writeFileSync(PLUGIN_HOOKS_PATH, want, "utf8");
|
|
3131
|
+
return true;
|
|
3132
|
+
}
|
|
3133
|
+
function mergeCursorHooks(settings, { hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {}) {
|
|
3134
|
+
settings.version = 1;
|
|
3135
|
+
settings.hooks ??= {};
|
|
3136
|
+
const added = [];
|
|
3137
|
+
const repaired = [];
|
|
3138
|
+
const removed = [];
|
|
3139
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3140
|
+
if (!Array.isArray(entries)) continue;
|
|
3141
|
+
const keep = entries.filter((raw) => {
|
|
3142
|
+
if (!isLegacyEntry(raw)) return true;
|
|
3143
|
+
removed.push(`${event}: ${commandOf(raw)}`);
|
|
3144
|
+
return false;
|
|
3145
|
+
});
|
|
3146
|
+
settings.hooks[event] = keep;
|
|
3147
|
+
}
|
|
3148
|
+
for (const entry of hooks) {
|
|
3149
|
+
settings.hooks[entry.event] ??= [];
|
|
3150
|
+
const entries = settings.hooks[entry.event];
|
|
3151
|
+
const identity = identityFor(entry);
|
|
3152
|
+
const existing = entries.filter(
|
|
3153
|
+
(raw2) => isShimEntry(raw2) && identityOfCommand(commandOf(raw2)) === identity
|
|
3154
|
+
);
|
|
3155
|
+
if (existing.length === 0) {
|
|
3156
|
+
entries.push(desiredEntry(entry, render));
|
|
3157
|
+
added.push(`${entry.event}/${entry.hook}`);
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
for (const extra of existing.slice(1)) {
|
|
3161
|
+
entries.splice(entries.indexOf(extra), 1);
|
|
3162
|
+
removed.push(`${entry.event}: duplicate ${entry.hook}`);
|
|
3163
|
+
}
|
|
3164
|
+
const raw = existing[0];
|
|
3165
|
+
if (!sameEntry(raw, entry, render)) {
|
|
3166
|
+
Object.assign(raw, desiredEntry(entry, render));
|
|
3167
|
+
if (!entry.matcher && "matcher" in raw) delete raw.matcher;
|
|
3168
|
+
repaired.push(`${entry.event}/${entry.hook}`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3172
|
+
if (!Array.isArray(entries)) continue;
|
|
3173
|
+
const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
|
|
3174
|
+
settings.hooks[event] = entries.filter((raw) => {
|
|
3175
|
+
if (!isShimEntry(raw)) return true;
|
|
3176
|
+
if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
|
|
3177
|
+
removed.push(`${event}: retired ${commandOf(raw)}`);
|
|
3178
|
+
return false;
|
|
3179
|
+
});
|
|
3180
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
3181
|
+
}
|
|
3182
|
+
return { added, repaired, removed };
|
|
3183
|
+
}
|
|
3184
|
+
function launcherPlan() {
|
|
3185
|
+
let existing = null;
|
|
3186
|
+
try {
|
|
3187
|
+
existing = fs2.readFileSync(LAUNCHER_PATH, "utf8");
|
|
3188
|
+
} catch {
|
|
3189
|
+
existing = null;
|
|
3190
|
+
}
|
|
3191
|
+
return planCustomerLauncherWrite(existing, "cursor");
|
|
3192
|
+
}
|
|
3193
|
+
function install({ json = false } = {}) {
|
|
3194
|
+
const settings = readSettings();
|
|
3195
|
+
const { added, repaired, removed } = mergeCursorHooks(settings);
|
|
3196
|
+
const settingsChanged = added.length + repaired.length + removed.length > 0;
|
|
3197
|
+
if (settingsChanged) writeSettings(settings);
|
|
3198
|
+
let launcherWritten = false;
|
|
3199
|
+
if (KIT.customer) {
|
|
3200
|
+
const plan = launcherPlan();
|
|
3201
|
+
if (plan.write) {
|
|
3202
|
+
fs2.mkdirSync(path3.dirname(LAUNCHER_PATH), { recursive: true });
|
|
3203
|
+
fs2.writeFileSync(LAUNCHER_PATH, plan.text, "utf8");
|
|
3204
|
+
launcherWritten = true;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const pluginChanged = KIT.customer ? false : syncPluginHooks();
|
|
3208
|
+
if (json) {
|
|
3209
|
+
console.log(
|
|
3210
|
+
JSON.stringify(
|
|
3211
|
+
{
|
|
3212
|
+
ok: true,
|
|
3213
|
+
changed: settingsChanged || pluginChanged || launcherWritten,
|
|
3214
|
+
added,
|
|
3215
|
+
repaired,
|
|
3216
|
+
removed,
|
|
3217
|
+
pluginChanged,
|
|
3218
|
+
launcherWritten,
|
|
3219
|
+
total: ACTIVE_HOOKS.length,
|
|
3220
|
+
customer: KIT.customer
|
|
3221
|
+
},
|
|
3222
|
+
null,
|
|
3223
|
+
2
|
|
3224
|
+
)
|
|
3225
|
+
);
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (pluginChanged) {
|
|
3229
|
+
console.log(`install-cursor-hooks: regenerated ${path3.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
|
|
3230
|
+
}
|
|
3231
|
+
if (launcherWritten) {
|
|
3232
|
+
console.log(`install-cursor-hooks: wrote ${path3.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
|
|
3233
|
+
}
|
|
3234
|
+
if (!settingsChanged) {
|
|
3235
|
+
console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const parts = [];
|
|
3239
|
+
if (added.length) parts.push(`added ${added.length}`);
|
|
3240
|
+
if (repaired.length) parts.push(`repaired ${repaired.length}`);
|
|
3241
|
+
if (removed.length) parts.push(`removed ${removed.length}`);
|
|
3242
|
+
console.log(
|
|
3243
|
+
`install-cursor-hooks: ${parts.join("; ")} \u2192 ${path3.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
|
|
3244
|
+
);
|
|
3245
|
+
for (const line of removed) console.log(` - ${line}`);
|
|
3246
|
+
console.log(
|
|
3247
|
+
KIT.customer ? ` Launcher: ${CUSTOMER_LAUNCHER} \u2192 node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)` : ` Shim: ${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)`
|
|
3248
|
+
);
|
|
3249
|
+
for (const gap of UNPORTABLE) {
|
|
3250
|
+
console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) \u2192 covered by ${gap.coveredBy}`);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function check({ json = false } = {}) {
|
|
3254
|
+
const settings = readSettings();
|
|
3255
|
+
const issues = [];
|
|
3256
|
+
const entriesFor = (event) => Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
|
|
3257
|
+
for (const entry of ACTIVE_HOOKS) {
|
|
3258
|
+
const identity = identityFor(entry);
|
|
3259
|
+
const ours = entriesFor(entry.event).filter(
|
|
3260
|
+
(raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity
|
|
3261
|
+
);
|
|
3262
|
+
if (ours.length === 0) {
|
|
3263
|
+
issues.push(`${entry.event}: missing ${entry.hook}`);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}\xD7`);
|
|
3267
|
+
for (const raw of ours) {
|
|
3268
|
+
if (!sameEntry(raw, entry)) {
|
|
3269
|
+
issues.push(
|
|
3270
|
+
`${entry.event}/${entry.hook}: drift \u2014 expected \`${KIT.commandFor(entry)}\` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? "none"}), found \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? "none"})`
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
3276
|
+
if (!Array.isArray(entries)) continue;
|
|
3277
|
+
for (const raw of entries) {
|
|
3278
|
+
if (isLegacyEntry(raw)) {
|
|
3279
|
+
issues.push(`${event}: superseded direct registration \u2014 ${commandOf(raw)}`);
|
|
3280
|
+
} else if (isShimEntry(raw) && !ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))) {
|
|
3281
|
+
issues.push(`${event}: not part of this repo's hook set \u2014 ${commandOf(raw)}`);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
let pluginOnDisk = null;
|
|
3286
|
+
try {
|
|
3287
|
+
pluginOnDisk = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
|
|
3288
|
+
} catch {
|
|
3289
|
+
pluginOnDisk = null;
|
|
3290
|
+
}
|
|
3291
|
+
if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
|
|
3292
|
+
issues.push(
|
|
3293
|
+
`plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? "missing" : "stale"} \u2014 regenerate with \`npm run hooks:install-cursor\``
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
if (KIT.customer) {
|
|
3297
|
+
const plan = launcherPlan();
|
|
3298
|
+
if (plan.write) {
|
|
3299
|
+
issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === "missing" ? "missing" : "stale"} \u2014 the hook rows point at it`);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
if (json) {
|
|
3303
|
+
console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
|
|
3304
|
+
process.exit(issues.length === 0 ? 0 : 1);
|
|
3305
|
+
}
|
|
3306
|
+
if (issues.length === 0) {
|
|
3307
|
+
console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
|
|
3308
|
+
process.exit(0);
|
|
3309
|
+
}
|
|
3310
|
+
console.error("install-cursor-hooks --check: drift detected");
|
|
3311
|
+
for (const issue of issues) console.error(` - ${issue}`);
|
|
3312
|
+
console.error(" Run `node scripts/install-cursor-hooks.mjs` to repair.");
|
|
3313
|
+
process.exit(1);
|
|
3314
|
+
}
|
|
3315
|
+
function uninstall() {
|
|
3316
|
+
const settings = readSettings();
|
|
3317
|
+
if (!settings.hooks) {
|
|
3318
|
+
console.log("install-cursor-hooks: nothing to uninstall (no hooks block).");
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
let removed = 0;
|
|
3322
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3323
|
+
if (!Array.isArray(entries)) continue;
|
|
3324
|
+
const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
|
|
3325
|
+
removed += entries.length - keep.length;
|
|
3326
|
+
if (keep.length === 0) delete settings.hooks[event];
|
|
3327
|
+
else settings.hooks[event] = keep;
|
|
3328
|
+
}
|
|
3329
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
3330
|
+
writeSettings(settings);
|
|
3331
|
+
console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? "y" : "ies"}.`);
|
|
3332
|
+
}
|
|
3333
|
+
if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
|
|
3334
|
+
const argv = process.argv.slice(2);
|
|
3335
|
+
const json = argv.includes("--json");
|
|
3336
|
+
if (argv.includes("--uninstall")) uninstall();
|
|
3337
|
+
else if (argv.includes("--check")) check({ json });
|
|
3338
|
+
else install({ json });
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
// spex-console/src/lib/cli/install.ts
|
|
3342
|
+
init_credentials();
|
|
3343
|
+
|
|
3344
|
+
// spex-console/src/lib/cli/connect-checks.ts
|
|
3345
|
+
import { dirname as dirname5 } from "node:path";
|
|
3346
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
3347
|
+
|
|
3348
|
+
// spex-console/src/lib/dev-mode.ts
|
|
3349
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3350
|
+
import { join as join10 } from "node:path";
|
|
3351
|
+
init_credentials();
|
|
3352
|
+
|
|
3353
|
+
// spex-console/src/lib/cli/connect-checks.ts
|
|
3354
|
+
init_credentials();
|
|
3355
|
+
|
|
3356
|
+
// spex-console/src/lib/cli/satellite.ts
|
|
3357
|
+
import { resolve as resolveNodePath } from "node:path";
|
|
3358
|
+
import process2 from "node:process";
|
|
3359
|
+
|
|
1547
3360
|
// spex-console/src/lib/cli/principle-selection.ts
|
|
1548
|
-
import { basename as
|
|
3361
|
+
import { basename as basename3 } from "node:path";
|
|
1549
3362
|
var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
|
|
1550
3363
|
function principleKey(row) {
|
|
1551
|
-
const base =
|
|
3364
|
+
const base = basename3(row.dest, ".md");
|
|
1552
3365
|
return base.startsWith("principle-") ? base.slice("principle-".length) : base;
|
|
1553
3366
|
}
|
|
1554
3367
|
function principleWikilink(row) {
|
|
1555
|
-
return
|
|
3368
|
+
return basename3(row.dest, ".md");
|
|
1556
3369
|
}
|
|
1557
3370
|
function parsePrinciplesFlag(value, rows) {
|
|
1558
3371
|
const principleRows = rows.filter((row) => row.group === "principle");
|
|
@@ -1614,8 +3427,247 @@ function principlesIndexHook(selected, rows) {
|
|
|
1614
3427
|
};
|
|
1615
3428
|
}
|
|
1616
3429
|
|
|
3430
|
+
// spex-console/src/lib/cli/receipt.ts
|
|
3431
|
+
var RECEIPT_AGREEMENT_FIELDS = [
|
|
3432
|
+
"vault_id",
|
|
3433
|
+
"workspace_id",
|
|
3434
|
+
"account_id",
|
|
3435
|
+
"active_vault_id"
|
|
3436
|
+
];
|
|
3437
|
+
var ALL_FIELDS = [
|
|
3438
|
+
...RECEIPT_AGREEMENT_FIELDS,
|
|
3439
|
+
"display_name",
|
|
3440
|
+
"archived"
|
|
3441
|
+
];
|
|
3442
|
+
|
|
3443
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3444
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
3445
|
+
|
|
3446
|
+
// spex-console/src/lib/indexer/parser.ts
|
|
3447
|
+
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
3448
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3449
|
+
import { promises as fs3 } from "node:fs";
|
|
3450
|
+
import path4 from "node:path";
|
|
3451
|
+
|
|
3452
|
+
// spex-console/src/lib/cli/install.ts
|
|
3453
|
+
var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
|
|
3454
|
+
var PORTABLE_MCP_CLI = Object.freeze({
|
|
3455
|
+
claude: `\${CLAUDE_PROJECT_DIR:-.}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`,
|
|
3456
|
+
cursor: `\${workspaceFolder}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`
|
|
3457
|
+
});
|
|
3458
|
+
function resolveCallerPath(p, env, fallbackCwd) {
|
|
3459
|
+
const trimmed = p?.trim() ?? "";
|
|
3460
|
+
if (!trimmed) return "";
|
|
3461
|
+
if (isAbsolute3(trimmed)) return trimmed;
|
|
3462
|
+
const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
|
|
3463
|
+
return resolveNodePath2(base, trimmed);
|
|
3464
|
+
}
|
|
3465
|
+
var DEFAULT_REMOTE_MCP_URL = "https://app.heyspex.com/api/mcp";
|
|
3466
|
+
var REMOTE_MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
3467
|
+
var PLUGIN_MARKETPLACE_NAME = "heyspex-marketplace";
|
|
3468
|
+
var PLUGIN_INSTALL_SPEC = `heyspex@${PLUGIN_MARKETPLACE_NAME}`;
|
|
3469
|
+
var WRITE_TARGET_KIND = {
|
|
3470
|
+
specsGitkeep: "specs_gitkeep",
|
|
3471
|
+
heyspexYaml: "heyspex_yaml",
|
|
3472
|
+
cursorMcp: "cursor_mcp",
|
|
3473
|
+
claudeMcp: "claude_mcp",
|
|
3474
|
+
claudeMd: "claude_md",
|
|
3475
|
+
agentsMd: "agents_md",
|
|
3476
|
+
statuslineVendor: "statusline_vendor",
|
|
3477
|
+
gitignore: "gitignore",
|
|
3478
|
+
claudeMdTemplate: "claude_md_template",
|
|
3479
|
+
agentsMdTemplate: "agents_md_template"
|
|
3480
|
+
};
|
|
3481
|
+
var INSTALL_WRITE_TARGETS = [
|
|
3482
|
+
{
|
|
3483
|
+
kind: WRITE_TARGET_KIND.specsGitkeep,
|
|
3484
|
+
rel: "specs/.gitkeep",
|
|
3485
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "specs", ".gitkeep"),
|
|
3486
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3487
|
+
},
|
|
3488
|
+
{
|
|
3489
|
+
kind: WRITE_TARGET_KIND.heyspexYaml,
|
|
3490
|
+
rel: "heyspex.yaml",
|
|
3491
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "heyspex.yaml"),
|
|
3492
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3493
|
+
},
|
|
3494
|
+
{
|
|
3495
|
+
kind: WRITE_TARGET_KIND.cursorMcp,
|
|
3496
|
+
rel: ".cursor/mcp.json",
|
|
3497
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".cursor", "mcp.json"),
|
|
3498
|
+
appliesTo: (args) => args.cursor
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
kind: WRITE_TARGET_KIND.claudeMcp,
|
|
3502
|
+
rel: ".mcp.json",
|
|
3503
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".mcp.json"),
|
|
3504
|
+
appliesTo: (args) => args.claude
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
kind: WRITE_TARGET_KIND.claudeMd,
|
|
3508
|
+
rel: "CLAUDE.md",
|
|
3509
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md"),
|
|
3510
|
+
appliesTo: (args, opts) => args.claude && opts.installContract
|
|
3511
|
+
},
|
|
3512
|
+
{
|
|
3513
|
+
kind: WRITE_TARGET_KIND.agentsMd,
|
|
3514
|
+
rel: "AGENTS.md",
|
|
3515
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md"),
|
|
3516
|
+
appliesTo: (_args, opts) => opts.installContract
|
|
3517
|
+
},
|
|
3518
|
+
// The customer-template step (--register-vault) writes the project contract
|
|
3519
|
+
// beside a hand-authored CLAUDE.md/AGENTS.md as <file>.template; a rerun
|
|
3520
|
+
// rewrites it when the generated text changed.
|
|
3521
|
+
{
|
|
3522
|
+
kind: WRITE_TARGET_KIND.claudeMdTemplate,
|
|
3523
|
+
rel: "CLAUDE.md.template",
|
|
3524
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md.template"),
|
|
3525
|
+
appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && args.claude && opts.installContract
|
|
3526
|
+
},
|
|
3527
|
+
{
|
|
3528
|
+
kind: WRITE_TARGET_KIND.agentsMdTemplate,
|
|
3529
|
+
rel: "AGENTS.md.template",
|
|
3530
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md.template"),
|
|
3531
|
+
appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && opts.installContract
|
|
3532
|
+
},
|
|
3533
|
+
{
|
|
3534
|
+
kind: WRITE_TARGET_KIND.statuslineVendor,
|
|
3535
|
+
rel: "scripts/claude-statusline",
|
|
3536
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "scripts", "claude-statusline"),
|
|
3537
|
+
appliesTo: (args) => (args.statusline ?? true) && args.claude
|
|
3538
|
+
},
|
|
3539
|
+
{
|
|
3540
|
+
kind: WRITE_TARGET_KIND.gitignore,
|
|
3541
|
+
rel: ".gitignore",
|
|
3542
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".gitignore"),
|
|
3543
|
+
// A satellite owns no vault content and writes no machine-local
|
|
3544
|
+
// manifests of its own (its .mcp.json/.cursor/mcp.json point AT the
|
|
3545
|
+
// attached vault, not a fresh one this repo would need to gitignore) --
|
|
3546
|
+
// only a real vault bootstrap gets one.
|
|
3547
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3548
|
+
}
|
|
3549
|
+
];
|
|
3550
|
+
var INSTALL_HELP = [
|
|
3551
|
+
"Usage:",
|
|
3552
|
+
" hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
|
|
3553
|
+
" [--no-cursor] [--no-claude] [--claude-apply]",
|
|
3554
|
+
" [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
|
|
3555
|
+
" [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
|
|
3556
|
+
" [--no-statusline]",
|
|
3557
|
+
" [--cc | --command-code] [--register-vault [--workspace <id>]]",
|
|
3558
|
+
" [--remote[=<url>]]",
|
|
3559
|
+
" hs install --attach-vault <vault-id|vault-path> [--target <dir>] [...]",
|
|
3560
|
+
"",
|
|
3561
|
+
"Bootstrap a project vault for HeySpex agent tooling:",
|
|
3562
|
+
" \u2022 specs/ + heyspex.yaml (via hs init)",
|
|
3563
|
+
" \u2022 .cursor/mcp.json \u2014 Cursor MCP (gitignored, machine-local path to platform MCP)",
|
|
3564
|
+
" \u2022 .mcp.json \u2014 Claude Code project MCP (absolute platform path)",
|
|
3565
|
+
" \u2022 CLAUDE.md / AGENTS.md \u2014 thin agent contract pointing at the HeySpex MCP server",
|
|
3566
|
+
" \u2022 agent kit (customer repos) \u2014 the customer-scoped HeySpex hooks in",
|
|
3567
|
+
" .claude/settings.json + .cursor/hooks.json and the /hs-* commands in",
|
|
3568
|
+
" .claude/commands + .cursor/commands, every platform path absolute.",
|
|
3569
|
+
" Scope table: scripts/lib/customer-kit.mjs. Re-runs are no-ops; a command",
|
|
3570
|
+
" file you edited is left alone (--force overwrites it). `hs doctor` flags",
|
|
3571
|
+
" a missing or stale kit.",
|
|
3572
|
+
"",
|
|
3573
|
+
"Satellite repo (--attach-vault): wire a CODE repo to an EXISTING vault instead",
|
|
3574
|
+
"of making it a vault. Writes the MCP manifests + agent contract only, with",
|
|
3575
|
+
"HEYSPEX_VAULT_ROOT pinned to the attached vault's absolute root -- no specs/,",
|
|
3576
|
+
"no heyspex.yaml, no new registry row. The target is the repo you are standing",
|
|
3577
|
+
"in (or --target), never the vault being attached.",
|
|
3578
|
+
"",
|
|
3579
|
+
"That pin IS the attachment, so it must land somewhere: --attach-vault refuses a",
|
|
3580
|
+
"flag combination that writes no stdio manifest (--no-cursor --no-claude), and",
|
|
3581
|
+
"refuses to re-pin over manifests that name a different vault unless you pass",
|
|
3582
|
+
"--force. Re-attaching to the SAME vault is a no-op and says so.",
|
|
3583
|
+
"",
|
|
3584
|
+
'--attach-vault --remote: the stdio pin under "heyspex" in .mcp.json is preserved',
|
|
3585
|
+
"(never replaced by the HTTP form, even with --force) and the cloud form is",
|
|
3586
|
+
'written under a second key, "heyspex-cloud" -- Claude Code registers both the',
|
|
3587
|
+
"attached-vault local server and the cloud server. Without --force, a pin that",
|
|
3588
|
+
"already names a DIFFERENT vault still refuses and names the conflicting key,",
|
|
3589
|
+
"same as the non-remote case.",
|
|
3590
|
+
"",
|
|
3591
|
+
"Verify with `hs connect --check` from the satellite -- it is the command that",
|
|
3592
|
+
"audits THIS repo's rows and reports the attachment. `hs doctor` resolves",
|
|
3593
|
+
"THROUGH the pin and audits the ATTACHED VAULT repo, so it reports that vault's",
|
|
3594
|
+
"connection state, not the satellite's.",
|
|
3595
|
+
"",
|
|
3596
|
+
"Conflict scan: every install checks the target repo against the known-conflict",
|
|
3597
|
+
"registry (beads, workflow-pm, bd hooks, \u2026). See:",
|
|
3598
|
+
" specs/protocols/customer-vault-local-install-protocol.md",
|
|
3599
|
+
"",
|
|
3600
|
+
"Run from the HeySpex platform repo, or pass --platform to its checkout.",
|
|
3601
|
+
"Target defaults to the current directory.",
|
|
3602
|
+
"",
|
|
3603
|
+
"Flags:",
|
|
3604
|
+
" --target <dir> Project repo to bootstrap (default: cwd).",
|
|
3605
|
+
" --platform <dir> HeySpex platform checkout (default: HS_INSTALL_PLATFORM or dispatcher root).",
|
|
3606
|
+
" --claude-apply Run `claude mcp add --scope project` after writing files.",
|
|
3607
|
+
" --plugin Print the Claude Code plugin registration block (self-hosted",
|
|
3608
|
+
" marketplace at the platform root \u2014 HS-QTBK prototype).",
|
|
3609
|
+
" --plugin-apply Run `claude plugin marketplace add` + `claude plugin install`",
|
|
3610
|
+
" after writing files. Marketplace + plugin names are read from",
|
|
3611
|
+
` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
|
|
3612
|
+
" --plugin-scope <s> Plugin install scope: user, project (default), or local.",
|
|
3613
|
+
" Implies --plugin.",
|
|
3614
|
+
" --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
|
|
3615
|
+
" target. Commands resolve node_modules/heyspex first, so an npm-only",
|
|
3616
|
+
" install (no heyspex repo checkout) works. Pick this OR the plugin.",
|
|
3617
|
+
" --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
|
|
3618
|
+
" `copilot plugin install <plugin>@<marketplace>` (the platform root is",
|
|
3619
|
+
" node_modules/heyspex for a published install).",
|
|
3620
|
+
" --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
|
|
3621
|
+
" Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
|
|
3622
|
+
" hooks file (both would fire every hook twice).",
|
|
3623
|
+
" --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
|
|
3624
|
+
` the local stdio server \u2014 the Streamable-HTTP form against`,
|
|
3625
|
+
` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
|
|
3626
|
+
` env reference \`\${${REMOTE_MCP_TOKEN_ENV}}\` (never the literal token).`,
|
|
3627
|
+
' With --attach-vault, the stdio pin survives under "heyspex" and',
|
|
3628
|
+
' this is written under "heyspex-cloud" instead (see above). If',
|
|
3629
|
+
" this machine already ran `hs login` / `hs mcp pair <code>`, the",
|
|
3630
|
+
" printed guidance resolves the bearer from",
|
|
3631
|
+
" ~/.heyspex/credentials.yaml automatically -- no manual export",
|
|
3632
|
+
" needed on that machine.",
|
|
3633
|
+
" --no-statusline Skip the branded statusline (statusLine entry in the target's",
|
|
3634
|
+
" .claude/settings.json + machine-level glyph install).",
|
|
3635
|
+
" --cc, --command-code Print `cmdc mcp add` PowerShell one-liner for Command Code.",
|
|
3636
|
+
" --attach-vault <v> SATELLITE mode: attach this repo to the EXISTING vault named",
|
|
3637
|
+
" by a registry id (`hs vault list`) or the vault repo's path,",
|
|
3638
|
+
" instead of scaffolding specs/ + heyspex.yaml here. The vault",
|
|
3639
|
+
" must already be registered on this machine. Mutually exclusive",
|
|
3640
|
+
" with --register-vault and --workspace, and with any flag",
|
|
3641
|
+
" combination that would leave no stdio manifest to pin.",
|
|
3642
|
+
" Combines with --remote (see --remote above): the pin and the",
|
|
3643
|
+
" cloud endpoint coexist in .mcp.json under separate keys.",
|
|
3644
|
+
" --register-vault Register this repo's vault into ~/.heyspex/registry.json during install,",
|
|
3645
|
+
" and -- when this machine is signed in -- on the console too; both",
|
|
3646
|
+
" sides are read back (GET /api/vaults) and printed as a receipt.",
|
|
3647
|
+
" Re-running changes nothing and sends no cloud write.",
|
|
3648
|
+
" --workspace <id> With --register-vault: ensure this workspace exists, make it active,",
|
|
3649
|
+
" and associate the vault with it (default: the vault's current",
|
|
3650
|
+
" workspace, else your per-account default ws-<account>).",
|
|
3651
|
+
" --no-cursor Skip .cursor/mcp.json.",
|
|
3652
|
+
" --no-claude Skip .mcp.json and Claude instructions.",
|
|
3653
|
+
" --force Overwrite existing MCP manifests and heyspex.yaml.",
|
|
3654
|
+
" --dry-run Print the plan without writing.",
|
|
3655
|
+
" --skip-conflict-check Skip the known-conflict registry scan.",
|
|
3656
|
+
" --json Machine-readable result.",
|
|
3657
|
+
" --account <email|id> Confirm the account this repo is attached to. An interactive",
|
|
3658
|
+
" install asks (continue / sign in as someone else / create an",
|
|
3659
|
+
" account / abort); a non-interactive one with --register-vault or",
|
|
3660
|
+
" --remote refuses without this or --yes. See `hs whoami`.",
|
|
3661
|
+
" --yes Confirm the signed-in account shown, without typing it.",
|
|
3662
|
+
" --help, -h Show this help.",
|
|
3663
|
+
"",
|
|
3664
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed \xB7 16 sign-in required \xB7",
|
|
3665
|
+
"12 console unreachable (account check) \xB7 17 account not confirmed \xB7",
|
|
3666
|
+
"19 the cloud half did not land (the receipt names the retry) \xB7 21 both sides read back and disagree"
|
|
3667
|
+
].join("\n");
|
|
3668
|
+
|
|
1617
3669
|
// spex-console/bin/run-entry.ts
|
|
1618
|
-
import
|
|
3670
|
+
import process3 from "node:process";
|
|
1619
3671
|
var BinExit = class extends Error {
|
|
1620
3672
|
code;
|
|
1621
3673
|
constructor(code) {
|
|
@@ -1624,35 +3676,35 @@ var BinExit = class extends Error {
|
|
|
1624
3676
|
this.code = code;
|
|
1625
3677
|
}
|
|
1626
3678
|
};
|
|
1627
|
-
function runEntry(name, main2,
|
|
1628
|
-
const { fallbackCode = 1, unhandledLabel = "unhandled" } =
|
|
3679
|
+
function runEntry(name, main2, options2 = {}) {
|
|
3680
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options2;
|
|
1629
3681
|
void (async () => {
|
|
1630
3682
|
try {
|
|
1631
3683
|
await main2();
|
|
1632
3684
|
} catch (err) {
|
|
1633
3685
|
if (err instanceof BinExit) {
|
|
1634
|
-
|
|
3686
|
+
process3.exitCode = err.code;
|
|
1635
3687
|
return;
|
|
1636
3688
|
}
|
|
1637
3689
|
const message = err instanceof Error ? err.message : String(err);
|
|
1638
|
-
|
|
3690
|
+
process3.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
1639
3691
|
`);
|
|
1640
|
-
|
|
3692
|
+
process3.exitCode = fallbackCode;
|
|
1641
3693
|
}
|
|
1642
3694
|
})();
|
|
1643
3695
|
}
|
|
1644
3696
|
|
|
1645
3697
|
// spex-console/bin/hs-init.ts
|
|
1646
|
-
var INTERACTIVE =
|
|
3698
|
+
var INTERACTIVE = process4.stdin.isTTY === true;
|
|
1647
3699
|
var PKG_ROOT = resolve5(
|
|
1648
|
-
resolveCodeRoot({ packageRoot: resolve5(
|
|
3700
|
+
resolveCodeRoot({ packageRoot: resolve5(dirname6(fileURLToPath4(import.meta.url)), "..", "..") })
|
|
1649
3701
|
);
|
|
1650
|
-
var CONFIG_SEED_PATH =
|
|
3702
|
+
var CONFIG_SEED_PATH = join11(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
|
|
1651
3703
|
function readConfigSeed() {
|
|
1652
3704
|
try {
|
|
1653
|
-
return
|
|
3705
|
+
return readFileSync11(CONFIG_SEED_PATH, "utf8");
|
|
1654
3706
|
} catch {
|
|
1655
|
-
|
|
3707
|
+
process4.stderr.write(
|
|
1656
3708
|
`hs init: warning \u2014 config seed template not found at ${CONFIG_SEED_PATH}; .heyspex/config.yaml was not seeded (run \`hs settings set <key> <value>\` to create it).
|
|
1657
3709
|
`
|
|
1658
3710
|
);
|
|
@@ -1660,7 +3712,7 @@ function readConfigSeed() {
|
|
|
1660
3712
|
}
|
|
1661
3713
|
}
|
|
1662
3714
|
async function askYesNo(question) {
|
|
1663
|
-
const rl = createInterface({ input:
|
|
3715
|
+
const rl = createInterface({ input: process4.stdin, output: process4.stdout });
|
|
1664
3716
|
try {
|
|
1665
3717
|
const answer = (await rl.question(`${question} `)).trim().toLowerCase();
|
|
1666
3718
|
return answer === "y" || answer === "yes" || answer === "";
|
|
@@ -1673,9 +3725,9 @@ async function resolvePrincipleSelection(principlesFlag) {
|
|
|
1673
3725
|
return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
|
|
1674
3726
|
}
|
|
1675
3727
|
if (INTERACTIVE) {
|
|
1676
|
-
|
|
3728
|
+
process4.stdout.write("Select which universal principle docs to ship with this vault:\n");
|
|
1677
3729
|
for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
|
|
1678
|
-
|
|
3730
|
+
process4.stdout.write(` - ${choice.key}: ${choice.summary}
|
|
1679
3731
|
`);
|
|
1680
3732
|
}
|
|
1681
3733
|
const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
|
|
@@ -1684,39 +3736,39 @@ async function resolvePrincipleSelection(principlesFlag) {
|
|
|
1684
3736
|
return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
|
|
1685
3737
|
}
|
|
1686
3738
|
async function main() {
|
|
1687
|
-
const repoRoot =
|
|
1688
|
-
const parsed = parseInitArgs(
|
|
3739
|
+
const repoRoot = process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : process4.cwd();
|
|
3740
|
+
const parsed = parseInitArgs(process4.argv.slice(3));
|
|
1689
3741
|
if (parsed.help) {
|
|
1690
|
-
|
|
3742
|
+
process4.stdout.write(`${HELP}
|
|
1691
3743
|
`);
|
|
1692
|
-
|
|
3744
|
+
process4.exitCode = EXIT.OK;
|
|
1693
3745
|
return;
|
|
1694
3746
|
}
|
|
1695
3747
|
if (parsed.error) {
|
|
1696
|
-
|
|
3748
|
+
process4.stderr.write(`hs init: ${parsed.error.message}
|
|
1697
3749
|
`);
|
|
1698
|
-
|
|
3750
|
+
process4.exitCode = parsed.error.code;
|
|
1699
3751
|
return;
|
|
1700
3752
|
}
|
|
1701
3753
|
const args = parsed.args;
|
|
1702
3754
|
if (args.customerVaultDir) {
|
|
1703
|
-
const here =
|
|
3755
|
+
const here = dirname6(fileURLToPath4(import.meta.url));
|
|
1704
3756
|
const platformRoot = resolve5(here, "..", "..");
|
|
1705
|
-
const targetDir = resolve5(args.customerVaultDir);
|
|
3757
|
+
const targetDir = resolve5(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
|
|
1706
3758
|
const deps = {
|
|
1707
3759
|
platformRoot,
|
|
1708
|
-
exists: (p) =>
|
|
1709
|
-
readdir: (p) =>
|
|
3760
|
+
exists: (p) => existsSync5(p),
|
|
3761
|
+
readdir: (p) => readdirSync2(p),
|
|
1710
3762
|
mkdirRecursive: (p) => {
|
|
1711
|
-
|
|
3763
|
+
mkdirSync6(p, { recursive: true });
|
|
1712
3764
|
},
|
|
1713
|
-
writeFile: (p, c) =>
|
|
3765
|
+
writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
|
|
1714
3766
|
copyFile: (src, dest) => copyFileSync(src, dest),
|
|
1715
|
-
joinPath: (...s) =>
|
|
3767
|
+
joinPath: (...s) => join11(...s),
|
|
1716
3768
|
readConfigSeed,
|
|
1717
3769
|
readFile: (p) => {
|
|
1718
3770
|
try {
|
|
1719
|
-
return
|
|
3771
|
+
return readFileSync11(p, "utf8");
|
|
1720
3772
|
} catch {
|
|
1721
3773
|
return null;
|
|
1722
3774
|
}
|
|
@@ -1725,9 +3777,9 @@ async function main() {
|
|
|
1725
3777
|
};
|
|
1726
3778
|
const principlesResult = await resolvePrincipleSelection(args.principles);
|
|
1727
3779
|
if (!principlesResult.ok) {
|
|
1728
|
-
|
|
3780
|
+
process4.stderr.write(`hs init: ${principlesResult.message}
|
|
1729
3781
|
`);
|
|
1730
|
-
|
|
3782
|
+
process4.exitCode = EXIT.INVALID_ARG;
|
|
1731
3783
|
return;
|
|
1732
3784
|
}
|
|
1733
3785
|
const selected = principlesResult.selected;
|
|
@@ -1746,7 +3798,7 @@ async function main() {
|
|
|
1746
3798
|
manifestFilter: principleManifestFilter(selected),
|
|
1747
3799
|
afterScaffold
|
|
1748
3800
|
});
|
|
1749
|
-
const sink2 = result2.isError ?
|
|
3801
|
+
const sink2 = result2.isError ? process4.stderr : process4.stdout;
|
|
1750
3802
|
if (args.json) {
|
|
1751
3803
|
sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
|
|
1752
3804
|
`);
|
|
@@ -1756,29 +3808,29 @@ async function main() {
|
|
|
1756
3808
|
for (const notice of notices) sink2.write(`${notice}
|
|
1757
3809
|
`);
|
|
1758
3810
|
}
|
|
1759
|
-
|
|
3811
|
+
process4.exitCode = result2.code;
|
|
1760
3812
|
return;
|
|
1761
3813
|
}
|
|
1762
3814
|
const result = runInit(args, {
|
|
1763
3815
|
repoRoot,
|
|
1764
|
-
fileExists: (p) =>
|
|
3816
|
+
fileExists: (p) => existsSync5(p),
|
|
1765
3817
|
readFile: (p) => {
|
|
1766
3818
|
try {
|
|
1767
|
-
return
|
|
3819
|
+
return readFileSync11(p, "utf8");
|
|
1768
3820
|
} catch {
|
|
1769
3821
|
return null;
|
|
1770
3822
|
}
|
|
1771
3823
|
},
|
|
1772
|
-
writeFile: (p, c) =>
|
|
1773
|
-
joinPath: (...s) =>
|
|
3824
|
+
writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
|
|
3825
|
+
joinPath: (...s) => join11(...s),
|
|
1774
3826
|
mkdirRecursive: (p) => {
|
|
1775
|
-
|
|
3827
|
+
mkdirSync6(p, { recursive: true });
|
|
1776
3828
|
},
|
|
1777
3829
|
readConfigSeed
|
|
1778
3830
|
});
|
|
1779
|
-
const sink = result.isError ?
|
|
3831
|
+
const sink = result.isError ? process4.stderr : process4.stdout;
|
|
1780
3832
|
sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
|
|
1781
3833
|
`);
|
|
1782
|
-
|
|
3834
|
+
process4.exitCode = result.code;
|
|
1783
3835
|
}
|
|
1784
3836
|
runEntry("hs init", main, { fallbackCode: EXIT.WRITE_FAILED });
|