@massa-ai/opencode-plugin 1.55.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-cli.js +734 -17
- package/dist/index.js +82 -3
- package/package.json +3 -3
package/dist/config-cli.js
CHANGED
|
@@ -601,15 +601,18 @@ var init_massa_ai_config = __esm(() => {
|
|
|
601
601
|
security: {
|
|
602
602
|
corsOrigins: []
|
|
603
603
|
},
|
|
604
|
-
scheduler: DEFAULT_SCHEDULER_CONFIG
|
|
604
|
+
scheduler: DEFAULT_SCHEDULER_CONFIG,
|
|
605
|
+
bootstrap: { rules: {} }
|
|
605
606
|
};
|
|
606
607
|
});
|
|
607
608
|
|
|
608
609
|
// ../../packages/shared/dist/config/config-loader.js
|
|
609
610
|
var exports_config_loader = {};
|
|
610
611
|
__export(exports_config_loader, {
|
|
612
|
+
writeRawConfig: () => writeRawConfig,
|
|
611
613
|
writeFileAtomically: () => writeFileAtomically,
|
|
612
614
|
saveConfig: () => saveConfig,
|
|
615
|
+
readRawConfigStrict: () => readRawConfigStrict,
|
|
613
616
|
migrateDataDirOnce: () => migrateDataDirOnce,
|
|
614
617
|
mergeSchedulerSection: () => mergeSchedulerSection,
|
|
615
618
|
loadRawUserConfig: () => loadRawUserConfig,
|
|
@@ -620,12 +623,23 @@ __export(exports_config_loader, {
|
|
|
620
623
|
getConfigForEnv: () => getConfigForEnv,
|
|
621
624
|
getConfigDir: () => getConfigDir,
|
|
622
625
|
configExists: () => configExists,
|
|
623
|
-
__resetMigrationForTests: () => __resetMigrationForTests
|
|
626
|
+
__resetMigrationForTests: () => __resetMigrationForTests,
|
|
627
|
+
ConfigWriteConflictError: () => ConfigWriteConflictError,
|
|
628
|
+
ConfigParseError: () => ConfigParseError
|
|
624
629
|
});
|
|
625
630
|
import fs from "fs";
|
|
626
631
|
import path3 from "path";
|
|
627
632
|
import os2 from "os";
|
|
628
633
|
import crypto from "crypto";
|
|
634
|
+
function readConfigFileOrEmpty() {
|
|
635
|
+
try {
|
|
636
|
+
return fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error?.code === "ENOENT")
|
|
639
|
+
return "";
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
629
643
|
function getConfigDir() {
|
|
630
644
|
return CONFIG_DIR;
|
|
631
645
|
}
|
|
@@ -692,6 +706,21 @@ function loadRawUserConfig() {
|
|
|
692
706
|
return {};
|
|
693
707
|
}
|
|
694
708
|
}
|
|
709
|
+
function readRawConfigStrict() {
|
|
710
|
+
const raw = readConfigFileOrEmpty();
|
|
711
|
+
if (raw === "")
|
|
712
|
+
return {};
|
|
713
|
+
let parsed;
|
|
714
|
+
try {
|
|
715
|
+
parsed = JSON.parse(raw);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
throw new ConfigParseError(CONFIG_FILE, error);
|
|
718
|
+
}
|
|
719
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
720
|
+
throw new ConfigParseError(CONFIG_FILE, new Error("parsed value is not a JSON object"));
|
|
721
|
+
}
|
|
722
|
+
return parsed;
|
|
723
|
+
}
|
|
695
724
|
function loadConfigSafe() {
|
|
696
725
|
try {
|
|
697
726
|
return loadConfig();
|
|
@@ -746,6 +775,43 @@ function writeFileAtomically(targetPath, content) {
|
|
|
746
775
|
function saveConfig(config) {
|
|
747
776
|
writeFileAtomically(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
748
777
|
}
|
|
778
|
+
function writeRawConfig(doc, opts) {
|
|
779
|
+
const onDiskAtStart = readConfigFileOrEmpty();
|
|
780
|
+
if (onDiskAtStart === opts.expectedBytes) {
|
|
781
|
+
writeFileAtomically(CONFIG_FILE, JSON.stringify(doc, null, 2));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
let original;
|
|
785
|
+
let current;
|
|
786
|
+
try {
|
|
787
|
+
original = opts.expectedBytes === "" ? {} : JSON.parse(opts.expectedBytes);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
throw new ConfigParseError(`${CONFIG_FILE} (caller-supplied expectedBytes)`, error);
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
current = onDiskAtStart === "" ? {} : JSON.parse(onDiskAtStart);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
throw new ConfigParseError(CONFIG_FILE, error);
|
|
795
|
+
}
|
|
796
|
+
const reapplied = { ...current };
|
|
797
|
+
const touchedKeys = new Set([...Object.keys(original), ...Object.keys(doc)]);
|
|
798
|
+
for (const key of touchedKeys) {
|
|
799
|
+
const before = JSON.stringify(original[key]);
|
|
800
|
+
const after = JSON.stringify(doc[key]);
|
|
801
|
+
if (before === after)
|
|
802
|
+
continue;
|
|
803
|
+
if (Object.prototype.hasOwnProperty.call(doc, key)) {
|
|
804
|
+
reapplied[key] = doc[key];
|
|
805
|
+
} else {
|
|
806
|
+
delete reapplied[key];
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const onDiskImmediatelyBeforeWrite = readConfigFileOrEmpty();
|
|
810
|
+
if (onDiskImmediatelyBeforeWrite !== onDiskAtStart) {
|
|
811
|
+
throw new ConfigWriteConflictError(CONFIG_FILE);
|
|
812
|
+
}
|
|
813
|
+
writeFileAtomically(CONFIG_FILE, JSON.stringify(reapplied, null, 2));
|
|
814
|
+
}
|
|
749
815
|
function initConfig() {
|
|
750
816
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
751
817
|
saveConfig(defaultMassaAiConfig);
|
|
@@ -772,12 +838,25 @@ function getConfigForEnv() {
|
|
|
772
838
|
env.ENABLE_METRICS = String(config.logging.enableMetrics);
|
|
773
839
|
return env;
|
|
774
840
|
}
|
|
775
|
-
var CONFIG_DIR, CONFIG_FILE, migrationAttempted = false, tempFileCounter = 0;
|
|
841
|
+
var CONFIG_DIR, CONFIG_FILE, ConfigParseError, ConfigWriteConflictError, migrationAttempted = false, tempFileCounter = 0;
|
|
776
842
|
var init_config_loader = __esm(() => {
|
|
777
843
|
init_massa_ai_config();
|
|
778
844
|
init_xdg();
|
|
779
845
|
CONFIG_DIR = configDir("massa-ai");
|
|
780
846
|
CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
|
|
847
|
+
ConfigParseError = class ConfigParseError extends Error {
|
|
848
|
+
constructor(filePath, cause) {
|
|
849
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
850
|
+
super(`Failed to parse ${filePath}: ${reason}`);
|
|
851
|
+
this.name = "ConfigParseError";
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
ConfigWriteConflictError = class ConfigWriteConflictError extends Error {
|
|
855
|
+
constructor(filePath) {
|
|
856
|
+
super(`${filePath} changed on disk twice while writing \u2014 refusing to overwrite a ` + `concurrent update. Re-read the file and retry.`);
|
|
857
|
+
this.name = "ConfigWriteConflictError";
|
|
858
|
+
}
|
|
859
|
+
};
|
|
781
860
|
});
|
|
782
861
|
|
|
783
862
|
// ../../packages/shared/dist/env.js
|
|
@@ -2622,12 +2701,585 @@ function findRepoRootWithMarker(startDir, marker, maxLevels) {
|
|
|
2622
2701
|
}
|
|
2623
2702
|
return null;
|
|
2624
2703
|
}
|
|
2625
|
-
//
|
|
2626
|
-
|
|
2704
|
+
// ../../packages/shared/dist/bootstrap/rules.js
|
|
2705
|
+
var BOOTSTRAP_RULE_IDS = [
|
|
2706
|
+
"caveman",
|
|
2707
|
+
"massa-ai-router",
|
|
2708
|
+
"persona-router",
|
|
2709
|
+
"dedupe-guardrails",
|
|
2710
|
+
"plan-challenge",
|
|
2711
|
+
"conversation-feedback",
|
|
2712
|
+
"indexing-hygiene",
|
|
2713
|
+
"english-code",
|
|
2714
|
+
"code-comments"
|
|
2715
|
+
];
|
|
2716
|
+
function isBootstrapRuleId(value) {
|
|
2717
|
+
return typeof value === "string" && BOOTSTRAP_RULE_IDS.includes(value);
|
|
2718
|
+
}
|
|
2719
|
+
var BOOTSTRAP_RULES = [
|
|
2720
|
+
{
|
|
2721
|
+
id: "caveman",
|
|
2722
|
+
defaultEnabled: true,
|
|
2723
|
+
description: "Keep communication compressed while preserving technical accuracy."
|
|
2724
|
+
},
|
|
2725
|
+
{
|
|
2726
|
+
id: "massa-ai-router",
|
|
2727
|
+
defaultEnabled: true,
|
|
2728
|
+
description: "Load the massa-ai skill as the workflow router before substantive work."
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
id: "persona-router",
|
|
2732
|
+
defaultEnabled: true,
|
|
2733
|
+
description: "Select one cataloged specialist persona after massa-ai context is available."
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
id: "dedupe-guardrails",
|
|
2737
|
+
defaultEnabled: true,
|
|
2738
|
+
description: "Reuse already-loaded massa-ai context instead of bulk-loading workflows or references."
|
|
2739
|
+
},
|
|
2740
|
+
{
|
|
2741
|
+
id: "plan-challenge",
|
|
2742
|
+
defaultEnabled: true,
|
|
2743
|
+
description: "Run The Fool as a post-plan challenge gate per the configured policy."
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
id: "conversation-feedback",
|
|
2747
|
+
defaultEnabled: true,
|
|
2748
|
+
description: "Emit chat-visible status updates for massa-ai workflow progress."
|
|
2749
|
+
},
|
|
2750
|
+
{
|
|
2751
|
+
id: "indexing-hygiene",
|
|
2752
|
+
defaultEnabled: true,
|
|
2753
|
+
description: "Ignore build output, dependency, and secret paths during indexing and context loading."
|
|
2754
|
+
},
|
|
2755
|
+
{
|
|
2756
|
+
id: "english-code",
|
|
2757
|
+
defaultEnabled: true,
|
|
2758
|
+
description: "Write generated code, identifiers, comments, and commit-facing artifacts in English regardless of conversational language."
|
|
2759
|
+
},
|
|
2760
|
+
{
|
|
2761
|
+
id: "code-comments",
|
|
2762
|
+
defaultEnabled: false,
|
|
2763
|
+
description: "Require API doc blocks and rationale comments on generated code, per code-annotation.md \xA71/\xA72."
|
|
2764
|
+
}
|
|
2765
|
+
];
|
|
2766
|
+
var RULES_BY_ID = new Map(BOOTSTRAP_RULES.map((rule) => [rule.id, rule]));
|
|
2767
|
+
function bootstrapRuleDefaults() {
|
|
2768
|
+
const defaults = {};
|
|
2769
|
+
for (const rule of BOOTSTRAP_RULES)
|
|
2770
|
+
defaults[rule.id] = rule.defaultEnabled;
|
|
2771
|
+
return defaults;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
class BootstrapRuleError extends Error {
|
|
2775
|
+
constructor(message) {
|
|
2776
|
+
super(message);
|
|
2777
|
+
this.name = "BootstrapRuleError";
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
function namedError4(name, message) {
|
|
2781
|
+
const err = new BootstrapRuleError(message);
|
|
2782
|
+
err.name = name;
|
|
2783
|
+
return err;
|
|
2784
|
+
}
|
|
2785
|
+
var UnknownRuleError = (id, known = BOOTSTRAP_RULE_IDS) => namedError4("UnknownRuleError", `unknown bootstrap rule "${id}" \u2014 valid ids: ${known.join(", ")}`);
|
|
2786
|
+
function assertKnownRuleId(id) {
|
|
2787
|
+
if (!isBootstrapRuleId(id))
|
|
2788
|
+
throw UnknownRuleError(id, BOOTSTRAP_RULE_IDS);
|
|
2789
|
+
}
|
|
2790
|
+
// ../../packages/shared/dist/bootstrap/state.js
|
|
2791
|
+
init_config_loader();
|
|
2792
|
+
import fs9 from "fs";
|
|
2793
|
+
var BOOTSTRAP_STATE_KEY = "bootstrap";
|
|
2794
|
+
var BOOTSTRAP_RULES_KEY = "rules";
|
|
2795
|
+
var BOOTSTRAP_STATE_PATH = `${BOOTSTRAP_STATE_KEY}.${BOOTSTRAP_RULES_KEY}`;
|
|
2796
|
+
function isPlainObject2(value) {
|
|
2797
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2798
|
+
}
|
|
2799
|
+
function resolveBootstrapState(doc) {
|
|
2800
|
+
const document = doc ?? readRawConfigStrict();
|
|
2801
|
+
const state = bootstrapRuleDefaults();
|
|
2802
|
+
const ignored = [];
|
|
2803
|
+
const bootstrap = document[BOOTSTRAP_STATE_KEY];
|
|
2804
|
+
if (bootstrap === undefined)
|
|
2805
|
+
return { state, ignoredStateKeys: [] };
|
|
2806
|
+
if (!isPlainObject2(bootstrap)) {
|
|
2807
|
+
return { state, ignoredStateKeys: [BOOTSTRAP_STATE_KEY] };
|
|
2808
|
+
}
|
|
2809
|
+
const rules = bootstrap[BOOTSTRAP_RULES_KEY];
|
|
2810
|
+
if (rules === undefined)
|
|
2811
|
+
return { state, ignoredStateKeys: [] };
|
|
2812
|
+
if (!isPlainObject2(rules)) {
|
|
2813
|
+
return { state, ignoredStateKeys: [BOOTSTRAP_STATE_PATH] };
|
|
2814
|
+
}
|
|
2815
|
+
for (const [key, value] of Object.entries(rules)) {
|
|
2816
|
+
if (!isBootstrapRuleId(key) || typeof value !== "boolean") {
|
|
2817
|
+
ignored.push(key);
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
state[key] = value;
|
|
2821
|
+
}
|
|
2822
|
+
return { state, ignoredStateKeys: ignored.sort() };
|
|
2823
|
+
}
|
|
2824
|
+
function readConfigBytes() {
|
|
2825
|
+
try {
|
|
2826
|
+
return fs9.readFileSync(getConfigPath(), "utf-8");
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
if (error?.code === "ENOENT")
|
|
2829
|
+
return "";
|
|
2830
|
+
throw error;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
function setBootstrapRuleEnabled(id, enabled) {
|
|
2834
|
+
assertKnownRuleId(id);
|
|
2835
|
+
const expectedBytes = readConfigBytes();
|
|
2836
|
+
let document;
|
|
2837
|
+
if (expectedBytes === "") {
|
|
2838
|
+
document = {};
|
|
2839
|
+
} else {
|
|
2840
|
+
let parsed;
|
|
2841
|
+
try {
|
|
2842
|
+
parsed = JSON.parse(expectedBytes);
|
|
2843
|
+
} catch (error) {
|
|
2844
|
+
throw new ConfigParseError(getConfigPath(), error);
|
|
2845
|
+
}
|
|
2846
|
+
if (!isPlainObject2(parsed)) {
|
|
2847
|
+
throw new ConfigParseError(getConfigPath(), new Error("parsed value is not a JSON object"));
|
|
2848
|
+
}
|
|
2849
|
+
document = parsed;
|
|
2850
|
+
}
|
|
2851
|
+
const before = resolveBootstrapState(document);
|
|
2852
|
+
const bootstrap = document[BOOTSTRAP_STATE_KEY];
|
|
2853
|
+
const bootstrapSubtree = isPlainObject2(bootstrap) ? bootstrap : {};
|
|
2854
|
+
const rules = bootstrapSubtree[BOOTSTRAP_RULES_KEY];
|
|
2855
|
+
const rulesSubtree = isPlainObject2(rules) ? rules : {};
|
|
2856
|
+
const next = {
|
|
2857
|
+
...document,
|
|
2858
|
+
[BOOTSTRAP_STATE_KEY]: {
|
|
2859
|
+
...bootstrapSubtree,
|
|
2860
|
+
[BOOTSTRAP_RULES_KEY]: { ...rulesSubtree, [id]: enabled }
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
writeRawConfig(next, { expectedBytes });
|
|
2864
|
+
const after = resolveBootstrapState(next);
|
|
2865
|
+
return {
|
|
2866
|
+
id,
|
|
2867
|
+
enabled,
|
|
2868
|
+
changed: before.state[id] !== enabled,
|
|
2869
|
+
state: after.state,
|
|
2870
|
+
ignoredStateKeys: after.ignoredStateKeys
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
// ../../packages/shared/dist/bootstrap/render.js
|
|
2627
2874
|
import path13 from "path";
|
|
2875
|
+
var BOOTSTRAP_BLOCK_START = "<!-- massa-ai:bootstrap:start -->";
|
|
2876
|
+
var BOOTSTRAP_BLOCK_END = "<!-- massa-ai:bootstrap:end -->";
|
|
2877
|
+
var CONTRACT_FILENAME = "MASSA-AI.md";
|
|
2878
|
+
function wrapBootstrapBlock(body) {
|
|
2879
|
+
return `${BOOTSTRAP_BLOCK_START}
|
|
2880
|
+
${body.replace(/\n+$/, "")}
|
|
2881
|
+
${BOOTSTRAP_BLOCK_END}
|
|
2882
|
+
`;
|
|
2883
|
+
}
|
|
2884
|
+
function ruleMarker(id, suffix) {
|
|
2885
|
+
return `<!-- massa-ai:rule:${id}:${suffix} -->`;
|
|
2886
|
+
}
|
|
2887
|
+
var ANY_MASSA_AI_MARKER = /<!--\s*massa-ai:(?:rule|bootstrap):[^>]*-->/;
|
|
2888
|
+
|
|
2889
|
+
class BootstrapRenderError extends Error {
|
|
2890
|
+
details;
|
|
2891
|
+
constructor(name, message, details = []) {
|
|
2892
|
+
super(message);
|
|
2893
|
+
this.name = name;
|
|
2894
|
+
this.details = details;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
var HOST_CONFIG_DIR = {
|
|
2898
|
+
claude: [".claude"],
|
|
2899
|
+
codex: [".codex"],
|
|
2900
|
+
cursor: [".cursor"],
|
|
2901
|
+
opencode: [".config", "opencode"]
|
|
2902
|
+
};
|
|
2903
|
+
function resolveHostRoot(host, targetHome, hostRoot) {
|
|
2904
|
+
requireAbsoluteTargetHome(targetHome);
|
|
2905
|
+
if (hostRoot === undefined)
|
|
2906
|
+
return path13.join(targetHome, ...HOST_CONFIG_DIR[host]);
|
|
2907
|
+
const relative = path13.relative(targetHome, hostRoot);
|
|
2908
|
+
if (!path13.isAbsolute(hostRoot) || relative === "" || relative.startsWith("..") || path13.isAbsolute(relative)) {
|
|
2909
|
+
throw new BootstrapRenderError("HostRootOutsideTargetHomeError", `hostRoot must be an absolute directory inside targetHome, got "${hostRoot}" for targetHome "${targetHome}"`, [hostRoot, targetHome]);
|
|
2910
|
+
}
|
|
2911
|
+
return hostRoot;
|
|
2912
|
+
}
|
|
2913
|
+
function bootstrapContractPath(host, targetHome, hostRoot) {
|
|
2914
|
+
return path13.join(resolveHostRoot(host, targetHome, hostRoot), CONTRACT_FILENAME);
|
|
2915
|
+
}
|
|
2916
|
+
function bootstrapStateFilePath(targetHome) {
|
|
2917
|
+
requireAbsoluteTargetHome(targetHome);
|
|
2918
|
+
return path13.join(targetHome, ".config", "massa-ai", "config.json");
|
|
2919
|
+
}
|
|
2920
|
+
function renderBootstrap(options) {
|
|
2921
|
+
const { source, state, host, targetHome, hostRoot } = options;
|
|
2922
|
+
requireAbsoluteTargetHome(targetHome);
|
|
2923
|
+
requireTotalState(state);
|
|
2924
|
+
const body = applyRuleState(extractBootstrapBlock(source), state);
|
|
2925
|
+
const contract = `${renderHeader(state, targetHome)}
|
|
2926
|
+
|
|
2927
|
+
${body}`;
|
|
2928
|
+
const pointer = renderPointer(host, targetHome, hostRoot);
|
|
2929
|
+
const emitted = [
|
|
2930
|
+
...contract.split(`
|
|
2931
|
+
`).filter((line) => ANY_MASSA_AI_MARKER.test(line)),
|
|
2932
|
+
...pointer.split(`
|
|
2933
|
+
`).filter((line) => ANY_MASSA_AI_MARKER.test(line))
|
|
2934
|
+
].map((line) => line.trim());
|
|
2935
|
+
if (emitted.length > 0) {
|
|
2936
|
+
throw new BootstrapRenderError("MarkerInInterpolatedPathError", `rendered output carries a massa-ai marker, which can only have come from an interpolated path: ${emitted.join(", ")}`, emitted);
|
|
2937
|
+
}
|
|
2938
|
+
return { contract, pointer };
|
|
2939
|
+
}
|
|
2940
|
+
function requireAbsoluteTargetHome(targetHome) {
|
|
2941
|
+
if (!path13.isAbsolute(targetHome)) {
|
|
2942
|
+
throw new BootstrapRenderError("TargetHomeNotAbsoluteError", `targetHome must be an absolute path, got "${targetHome}"`, [targetHome]);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function requireTotalState(state) {
|
|
2946
|
+
const missing = BOOTSTRAP_RULES.filter((rule) => typeof state[rule.id] !== "boolean").map((rule) => rule.id);
|
|
2947
|
+
if (missing.length > 0) {
|
|
2948
|
+
throw new BootstrapRenderError("IncompleteBootstrapStateError", `bootstrap state is missing a boolean for: ${missing.join(", ")} \u2014 pass a state resolved by resolveBootstrapState`, missing);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function extractBootstrapBlock(source) {
|
|
2952
|
+
const startCount = countOccurrences(source, BOOTSTRAP_BLOCK_START);
|
|
2953
|
+
const endCount = countOccurrences(source, BOOTSTRAP_BLOCK_END);
|
|
2954
|
+
const startIndex = source.indexOf(BOOTSTRAP_BLOCK_START);
|
|
2955
|
+
const endIndex = source.indexOf(BOOTSTRAP_BLOCK_END);
|
|
2956
|
+
if (startCount !== 1 || endCount !== 1 || startIndex > endIndex) {
|
|
2957
|
+
throw new BootstrapRenderError("BootstrapSourceError", `source must contain exactly one well-formed bootstrap block \u2014 found ${startCount} start and ${endCount} end marker(s)`, [`start=${startCount}`, `end=${endCount}`]);
|
|
2958
|
+
}
|
|
2959
|
+
return source.slice(startIndex + BOOTSTRAP_BLOCK_START.length, endIndex);
|
|
2960
|
+
}
|
|
2961
|
+
function findMarker(lines, marker) {
|
|
2962
|
+
let index = -1;
|
|
2963
|
+
let count = 0;
|
|
2964
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2965
|
+
if (lines[i]?.trim() === marker) {
|
|
2966
|
+
if (count === 0)
|
|
2967
|
+
index = i;
|
|
2968
|
+
count++;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return { index, count };
|
|
2972
|
+
}
|
|
2973
|
+
function countOccurrences(haystack, needle) {
|
|
2974
|
+
let count = 0;
|
|
2975
|
+
let from = 0;
|
|
2976
|
+
for (;; ) {
|
|
2977
|
+
const at = haystack.indexOf(needle, from);
|
|
2978
|
+
if (at === -1)
|
|
2979
|
+
return count;
|
|
2980
|
+
count++;
|
|
2981
|
+
from = at + needle.length;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
function splitOffSpan(inner, id) {
|
|
2985
|
+
const offStart = findMarker(inner, ruleMarker(id, "off"));
|
|
2986
|
+
const offEnd = findMarker(inner, ruleMarker(id, "off-end"));
|
|
2987
|
+
if (offStart.count === 0 && offEnd.count === 0)
|
|
2988
|
+
return { on: inner, off: [] };
|
|
2989
|
+
if (offStart.count !== 1 || offEnd.count !== 1 || offStart.index > offEnd.index) {
|
|
2990
|
+
throw new BootstrapRenderError("MalformedOffSpanError", `rule "${id}" has a malformed off-text span \u2014 found ${offStart.count} ":off" and ${offEnd.count} ":off-end" marker(s)`, [id]);
|
|
2991
|
+
}
|
|
2992
|
+
return {
|
|
2993
|
+
on: [...inner.slice(0, offStart.index), ...inner.slice(offEnd.index + 1)],
|
|
2994
|
+
off: inner.slice(offStart.index + 1, offEnd.index)
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
function applyRuleState(block, state) {
|
|
2998
|
+
const lines = block.split(`
|
|
2999
|
+
`);
|
|
3000
|
+
const missingSpans = [];
|
|
3001
|
+
for (const rule of BOOTSTRAP_RULES) {
|
|
3002
|
+
const start = findMarker(lines, ruleMarker(rule.id, "start"));
|
|
3003
|
+
const end = findMarker(lines, ruleMarker(rule.id, "end"));
|
|
3004
|
+
if (start.count !== 1 || end.count !== 1 || start.index > end.index) {
|
|
3005
|
+
missingSpans.push(rule.id);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
const span = splitOffSpan(lines.slice(start.index + 1, end.index), rule.id);
|
|
3009
|
+
const replacement = state[rule.id] ? span.on : span.off;
|
|
3010
|
+
lines.splice(start.index, end.index - start.index + 1, ...replacement);
|
|
3011
|
+
}
|
|
3012
|
+
if (missingSpans.length > 0) {
|
|
3013
|
+
throw new BootstrapRenderError("MissingRuleSpanError", `source has no well-formed span for rule(s): ${missingSpans.join(", ")}`, missingSpans);
|
|
3014
|
+
}
|
|
3015
|
+
const leftovers = lines.filter((line) => ANY_MASSA_AI_MARKER.test(line)).map((line) => line.trim());
|
|
3016
|
+
if (leftovers.length > 0) {
|
|
3017
|
+
throw new BootstrapRenderError("UnknownRuleMarkerError", `source carries marker(s) no registry rule consumed: ${leftovers.join(", ")}`, leftovers);
|
|
3018
|
+
}
|
|
3019
|
+
return normalizeBlankLines(lines);
|
|
3020
|
+
}
|
|
3021
|
+
function normalizeBlankLines(lines) {
|
|
3022
|
+
const out = [];
|
|
3023
|
+
let inFence = false;
|
|
3024
|
+
for (const line of lines) {
|
|
3025
|
+
if (line.trimStart().startsWith("```")) {
|
|
3026
|
+
inFence = !inFence;
|
|
3027
|
+
out.push(line);
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
if (inFence) {
|
|
3031
|
+
out.push(line);
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
const previous = out[out.length - 1];
|
|
3035
|
+
const isBlank = line.trim() === "";
|
|
3036
|
+
if (isBlank && (previous === undefined || previous.trim() === ""))
|
|
3037
|
+
continue;
|
|
3038
|
+
if (/^#{1,6} /.test(line) && previous !== undefined && previous.trim() !== "")
|
|
3039
|
+
out.push("");
|
|
3040
|
+
out.push(line);
|
|
3041
|
+
}
|
|
3042
|
+
return `${out.join(`
|
|
3043
|
+
`).trimEnd()}
|
|
3044
|
+
`;
|
|
3045
|
+
}
|
|
3046
|
+
function renderHeader(state, targetHome) {
|
|
3047
|
+
const lines = [
|
|
3048
|
+
"> Generated by massa-ai from `skills/AGENTS.md`. Edits made here are lost on",
|
|
3049
|
+
"> the next `scripts/install-skills.sh --apply` or",
|
|
3050
|
+
"> `massa-ai-config bootstrap enable|disable <rule-id>` run.",
|
|
3051
|
+
">",
|
|
3052
|
+
`> Rule state lives in \`${bootstrapStateFilePath(targetHome)}\` under the`,
|
|
3053
|
+
`> \`${BOOTSTRAP_STATE_PATH}\` key. List every rule and its state with`,
|
|
3054
|
+
"> `massa-ai-config bootstrap list`; switch one back on with",
|
|
3055
|
+
"> `massa-ai-config bootstrap enable <rule-id>`. That command is a binary and",
|
|
3056
|
+
"> not a rule, so it keeps working with every rule below disabled \u2014",
|
|
3057
|
+
"> `massa-ai-config bootstrap enable massa-ai-router` is the way back when the",
|
|
3058
|
+
"> router rule itself is off."
|
|
3059
|
+
];
|
|
3060
|
+
if (BOOTSTRAP_RULES.every((rule) => !state[rule.id])) {
|
|
3061
|
+
lines.push("", "**Every massa-ai bootstrap rule is disabled.** The contract below carries no", "rule to activate, and nothing in this file changes agent behavior until at", "least one rule is enabled again.");
|
|
3062
|
+
}
|
|
3063
|
+
return lines.join(`
|
|
3064
|
+
`);
|
|
3065
|
+
}
|
|
3066
|
+
function renderPointer(host, targetHome, hostRoot) {
|
|
3067
|
+
return [
|
|
3068
|
+
"## massa-ai Startup Contract",
|
|
3069
|
+
"",
|
|
3070
|
+
"Before substantive work in this session, read",
|
|
3071
|
+
`\`${bootstrapContractPath(host, targetHome, hostRoot)}\``,
|
|
3072
|
+
"with your Read tool and follow it. This block is a pointer only: it states no",
|
|
3073
|
+
"rule of its own, and massa-ai overwrites it on the next install.",
|
|
3074
|
+
""
|
|
3075
|
+
].join(`
|
|
3076
|
+
`);
|
|
3077
|
+
}
|
|
3078
|
+
// ../../packages/shared/dist/bootstrap/report.js
|
|
3079
|
+
var CLEAN_STATUSES = new Set(["written", "skipped"]);
|
|
3080
|
+
function bootstrapReportSucceeded(report) {
|
|
3081
|
+
return report.rows.every((row) => CLEAN_STATUSES.has(row.status));
|
|
3082
|
+
}
|
|
3083
|
+
function buildBootstrapReport(input) {
|
|
3084
|
+
const restartRequired = !input.dryRun && input.rows.some((row) => row.status === "written");
|
|
3085
|
+
return {
|
|
3086
|
+
rows: input.rows,
|
|
3087
|
+
restartRequired,
|
|
3088
|
+
dryRun: input.dryRun,
|
|
3089
|
+
ignoredStateKeys: input.ignoredStateKeys
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
// ../../packages/shared/dist/bootstrap/engine.js
|
|
3093
|
+
init_config_loader();
|
|
3094
|
+
import fs10 from "fs";
|
|
3095
|
+
import path14 from "path";
|
|
3096
|
+
var INSTALL_STATE_FILENAME = "install-state.json";
|
|
3097
|
+
var WIRING_REMEDY = "scripts/install-skills.sh --apply";
|
|
3098
|
+
|
|
3099
|
+
class BootstrapEngineError extends Error {
|
|
3100
|
+
details;
|
|
3101
|
+
constructor(name, message, details = []) {
|
|
3102
|
+
super(message);
|
|
3103
|
+
this.name = name;
|
|
3104
|
+
this.details = details;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function applyBootstrapState(options) {
|
|
3108
|
+
const { targetHome } = options;
|
|
3109
|
+
const dryRun = options.dryRun ?? false;
|
|
3110
|
+
const warn = options.onWarning ?? ((message) => console.warn(message));
|
|
3111
|
+
const configPath = bootstrapStateFilePath(targetHome);
|
|
3112
|
+
const installStatePath = path14.join(path14.dirname(configPath), INSTALL_STATE_FILENAME);
|
|
3113
|
+
const { platforms } = readInstallState(installStatePath);
|
|
3114
|
+
const installed = HOSTS.filter((host) => platforms[host] !== undefined);
|
|
3115
|
+
if (installed.length === 0) {
|
|
3116
|
+
return buildBootstrapReport({ rows: [], dryRun, ignoredStateKeys: [] });
|
|
3117
|
+
}
|
|
3118
|
+
const resolved = resolveRuleState(configPath, warn);
|
|
3119
|
+
const source = readSource(options);
|
|
3120
|
+
const rows = installed.map((host) => applyHost({
|
|
3121
|
+
host,
|
|
3122
|
+
source,
|
|
3123
|
+
state: resolved.state,
|
|
3124
|
+
targetHome,
|
|
3125
|
+
hostRoot: recordedHostRoot(platforms[host]),
|
|
3126
|
+
dryRun
|
|
3127
|
+
}));
|
|
3128
|
+
return buildBootstrapReport({
|
|
3129
|
+
rows,
|
|
3130
|
+
dryRun,
|
|
3131
|
+
ignoredStateKeys: resolved.ignoredStateKeys
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
function recordedHostRoot(record) {
|
|
3135
|
+
const root = record?.root;
|
|
3136
|
+
return typeof root === "string" && root.length > 0 ? root : undefined;
|
|
3137
|
+
}
|
|
3138
|
+
function resolveRuleState(configPath, warn) {
|
|
3139
|
+
const raw = readFileOrNull(configPath);
|
|
3140
|
+
if (raw === null)
|
|
3141
|
+
return resolveBootstrapState({});
|
|
3142
|
+
let parsed;
|
|
3143
|
+
try {
|
|
3144
|
+
parsed = JSON.parse(raw);
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
warn(degradeWarning(configPath, error));
|
|
3147
|
+
return resolveBootstrapState({});
|
|
3148
|
+
}
|
|
3149
|
+
if (!isPlainObject3(parsed)) {
|
|
3150
|
+
warn(degradeWarning(configPath, new Error("parsed value is not a JSON object")));
|
|
3151
|
+
return resolveBootstrapState({});
|
|
3152
|
+
}
|
|
3153
|
+
return resolveBootstrapState(parsed);
|
|
3154
|
+
}
|
|
3155
|
+
function degradeWarning(configPath, cause) {
|
|
3156
|
+
const error = new ConfigParseError(configPath, cause);
|
|
3157
|
+
return `${error.name}: ${error.message} \u2014 rendering the registry bootstrap defaults; ${configPath} was not written`;
|
|
3158
|
+
}
|
|
3159
|
+
function readSource(options) {
|
|
3160
|
+
if (options.source !== undefined)
|
|
3161
|
+
return options.source;
|
|
3162
|
+
if (options.sourcePath !== undefined) {
|
|
3163
|
+
const text = readFileOrNull(options.sourcePath);
|
|
3164
|
+
if (text === null) {
|
|
3165
|
+
throw new BootstrapEngineError("BootstrapSourceUnreadableError", `could not read the bootstrap source at ${options.sourcePath}`, [options.sourcePath]);
|
|
3166
|
+
}
|
|
3167
|
+
return text;
|
|
3168
|
+
}
|
|
3169
|
+
throw new BootstrapEngineError("BootstrapSourceUnavailableError", "no bootstrap source given \u2014 pass `source` (the skills/AGENTS.md text) or `sourcePath`", ["source", "sourcePath"]);
|
|
3170
|
+
}
|
|
3171
|
+
function applyHost(input) {
|
|
3172
|
+
const { host, source, state, targetHome, hostRoot, dryRun } = input;
|
|
3173
|
+
let contractPath;
|
|
3174
|
+
let document;
|
|
3175
|
+
try {
|
|
3176
|
+
contractPath = bootstrapContractPath(host, targetHome, hostRoot);
|
|
3177
|
+
document = wrapBootstrapBlock(renderBootstrap({ source, state, host, targetHome, hostRoot }).contract);
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
return { host, status: "failed", reason: error.message };
|
|
3180
|
+
}
|
|
3181
|
+
const wired = isWired(host, targetHome, hostRoot);
|
|
3182
|
+
const notWired = () => ({
|
|
3183
|
+
host,
|
|
3184
|
+
status: "written-not-wired",
|
|
3185
|
+
reason: notWiredReason(host, targetHome, hostRoot)
|
|
3186
|
+
});
|
|
3187
|
+
if (readFileOrNull(contractPath) === document) {
|
|
3188
|
+
return wired ? { host, status: "skipped", reason: `${contractPath} is already up to date` } : notWired();
|
|
3189
|
+
}
|
|
3190
|
+
if (!dryRun) {
|
|
3191
|
+
try {
|
|
3192
|
+
writeFileAtomically(contractPath, document);
|
|
3193
|
+
} catch (error) {
|
|
3194
|
+
return {
|
|
3195
|
+
host,
|
|
3196
|
+
status: "failed",
|
|
3197
|
+
reason: `could not write ${contractPath}: ${error.message}`
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return wired ? { host, status: "written" } : notWired();
|
|
3202
|
+
}
|
|
3203
|
+
function wiringArtifact(host, targetHome, hostRoot) {
|
|
3204
|
+
const root = resolveHostRoot(host, targetHome, hostRoot);
|
|
3205
|
+
const contractPath = path14.join(root, CONTRACT_FILENAME);
|
|
3206
|
+
switch (host) {
|
|
3207
|
+
case "claude":
|
|
3208
|
+
return { file: path14.join(root, "CLAUDE.md"), token: `@${CONTRACT_FILENAME}` };
|
|
3209
|
+
case "codex":
|
|
3210
|
+
case "cursor":
|
|
3211
|
+
return { file: path14.join(root, "AGENTS.md"), token: contractPath };
|
|
3212
|
+
case "opencode":
|
|
3213
|
+
return { file: openCodeConfigPath(root), token: `"${contractPath}"` };
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
function openCodeConfigPath(root) {
|
|
3217
|
+
const json = path14.join(root, "opencode.json");
|
|
3218
|
+
if (fs10.existsSync(json))
|
|
3219
|
+
return json;
|
|
3220
|
+
return path14.join(root, "opencode.jsonc");
|
|
3221
|
+
}
|
|
3222
|
+
function isWired(host, targetHome, hostRoot) {
|
|
3223
|
+
const artifact = wiringArtifact(host, targetHome, hostRoot);
|
|
3224
|
+
const text = readFileOrNull(artifact.file);
|
|
3225
|
+
return text !== null && text.includes(artifact.token);
|
|
3226
|
+
}
|
|
3227
|
+
function notWiredReason(host, targetHome, hostRoot) {
|
|
3228
|
+
const artifact = wiringArtifact(host, targetHome, hostRoot);
|
|
3229
|
+
return `contract written, but ${host} has no artifact that loads it \u2014 expected ${artifact.token} in ${artifact.file}; run ${WIRING_REMEDY} to add the wiring`;
|
|
3230
|
+
}
|
|
3231
|
+
function readFileOrNull(filePath) {
|
|
3232
|
+
try {
|
|
3233
|
+
return fs10.readFileSync(filePath, "utf-8");
|
|
3234
|
+
} catch {
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function isPlainObject3(value) {
|
|
3239
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3240
|
+
}
|
|
3241
|
+
// ../../packages/shared/dist/bootstrap/format.js
|
|
3242
|
+
function enabledWord(enabled) {
|
|
3243
|
+
return enabled ? "enabled" : "disabled";
|
|
3244
|
+
}
|
|
3245
|
+
function formatBootstrapInventory(state) {
|
|
3246
|
+
return BOOTSTRAP_RULES.map((rule) => {
|
|
3247
|
+
const current = enabledWord(state[rule.id]);
|
|
3248
|
+
const fallback = enabledWord(rule.defaultEnabled);
|
|
3249
|
+
return ` ${rule.id}: ${current} (default: ${fallback}) \u2014 ${rule.description}`;
|
|
3250
|
+
}).join(`
|
|
3251
|
+
`);
|
|
3252
|
+
}
|
|
3253
|
+
function formatBootstrapReport(report) {
|
|
3254
|
+
const dryRunSuffix = report.dryRun ? " (dry run \u2014 no files changed)" : "";
|
|
3255
|
+
const lines = [];
|
|
3256
|
+
if (report.rows.length === 0) {
|
|
3257
|
+
lines.push(`bootstrap: no host installed${dryRunSuffix}`);
|
|
3258
|
+
} else {
|
|
3259
|
+
lines.push(`bootstrap: ${report.rows.length} host(s)${dryRunSuffix}`);
|
|
3260
|
+
for (const row of report.rows) {
|
|
3261
|
+
const detail = row.reason ? `: ${row.reason}` : "";
|
|
3262
|
+
lines.push(` ${row.host}: ${row.status}${detail}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
if (report.ignoredStateKeys.length > 0) {
|
|
3266
|
+
lines.push("", `Ignored persisted rule state: ${report.ignoredStateKeys.join(", ")} \u2014 not a known rule id with a boolean value.`);
|
|
3267
|
+
}
|
|
3268
|
+
if (report.restartRequired) {
|
|
3269
|
+
lines.push("", "A host session restart is required for the change to take effect.");
|
|
3270
|
+
}
|
|
3271
|
+
return lines.join(`
|
|
3272
|
+
`);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// ../../packages/shared/dist/bootstrap/index.js
|
|
3276
|
+
init_config_loader();
|
|
3277
|
+
// src/config-cli.ts
|
|
3278
|
+
import { promises as fs11 } from "fs";
|
|
3279
|
+
import path15 from "path";
|
|
2628
3280
|
import os8 from "os";
|
|
2629
3281
|
import { fileURLToPath } from "url";
|
|
2630
|
-
var __dirname2 =
|
|
3282
|
+
var __dirname2 = path15.dirname(fileURLToPath(import.meta.url));
|
|
2631
3283
|
var GENERATOR_MARKER = "scripts/generate-subagent-artifacts.ts";
|
|
2632
3284
|
var GENERATOR_MARKER_MAX_LEVELS = 6;
|
|
2633
3285
|
function formatVariantSync(results) {
|
|
@@ -2667,6 +3319,13 @@ Commands:
|
|
|
2667
3319
|
profile set <name> [--host <h>] [--dry-run]
|
|
2668
3320
|
Switch installed agents to a profile (restart required after)
|
|
2669
3321
|
|
|
3322
|
+
bootstrap list List every startup-contract rule: state, default, description
|
|
3323
|
+
bootstrap show Same as 'bootstrap list'
|
|
3324
|
+
bootstrap enable <rule-id> [--target <dir> --yes] [--dry-run]
|
|
3325
|
+
bootstrap disable <rule-id> [--target <dir> --yes] [--dry-run]
|
|
3326
|
+
Toggle one rule and re-render MASSA-AI.md for every
|
|
3327
|
+
recorded host (restart required after)
|
|
3328
|
+
|
|
2670
3329
|
Examples:
|
|
2671
3330
|
massa-ai-config init
|
|
2672
3331
|
massa-ai-config init --mistral your-api-key
|
|
@@ -2675,6 +3334,8 @@ Examples:
|
|
|
2675
3334
|
massa-ai-config set embedding.dimensions 1024
|
|
2676
3335
|
massa-ai-config agents install --user
|
|
2677
3336
|
massa-ai-config profile set work --dry-run
|
|
3337
|
+
massa-ai-config bootstrap list
|
|
3338
|
+
massa-ai-config bootstrap disable caveman
|
|
2678
3339
|
`);
|
|
2679
3340
|
}
|
|
2680
3341
|
function parseOptions(args) {
|
|
@@ -2838,18 +3499,18 @@ Using defaults:`);
|
|
|
2838
3499
|
return 1;
|
|
2839
3500
|
}
|
|
2840
3501
|
const scope = typeof options.project === "boolean" ? "project" : "user";
|
|
2841
|
-
const agentsDir = scope === "project" ?
|
|
2842
|
-
const sourceAgentsDir =
|
|
3502
|
+
const agentsDir = scope === "project" ? path15.join(process.cwd(), ".opencode/agents") : path15.join(process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() || path15.join(os8.homedir(), ".config"), "opencode", "agents");
|
|
3503
|
+
const sourceAgentsDir = path15.resolve(__dirname2, "..", "agents");
|
|
2843
3504
|
if (subcommand === "install") {
|
|
2844
|
-
await
|
|
3505
|
+
await fs11.mkdir(agentsDir, { recursive: true });
|
|
2845
3506
|
let count = 0;
|
|
2846
|
-
const entries = await
|
|
3507
|
+
const entries = await fs11.readdir(sourceAgentsDir);
|
|
2847
3508
|
for (const entry of entries) {
|
|
2848
3509
|
if (!entry.startsWith("massa-ai-") || !entry.endsWith(".md"))
|
|
2849
3510
|
continue;
|
|
2850
|
-
const src =
|
|
2851
|
-
const dest =
|
|
2852
|
-
await
|
|
3511
|
+
const src = path15.join(sourceAgentsDir, entry);
|
|
3512
|
+
const dest = path15.join(agentsDir, entry);
|
|
3513
|
+
await fs11.copyFile(src, dest);
|
|
2853
3514
|
count++;
|
|
2854
3515
|
}
|
|
2855
3516
|
console.log(`+ ${count} subagent specialists (generated from skills/agents/*/SKILL.md)`);
|
|
@@ -2857,14 +3518,14 @@ Using defaults:`);
|
|
|
2857
3518
|
} else {
|
|
2858
3519
|
let removed = 0;
|
|
2859
3520
|
try {
|
|
2860
|
-
const entries = await
|
|
3521
|
+
const entries = await fs11.readdir(agentsDir);
|
|
2861
3522
|
for (const entry of entries) {
|
|
2862
3523
|
if (!entry.startsWith("massa-ai-") || !entry.endsWith(".md"))
|
|
2863
3524
|
continue;
|
|
2864
|
-
const filePath =
|
|
2865
|
-
const content = await
|
|
3525
|
+
const filePath = path15.join(agentsDir, entry);
|
|
3526
|
+
const content = await fs11.readFile(filePath, "utf8");
|
|
2866
3527
|
if (content.includes("massa-ai-owned: true")) {
|
|
2867
|
-
await
|
|
3528
|
+
await fs11.unlink(filePath);
|
|
2868
3529
|
removed++;
|
|
2869
3530
|
}
|
|
2870
3531
|
}
|
|
@@ -2917,6 +3578,62 @@ Using defaults:`);
|
|
|
2917
3578
|
console.error("Usage: massa-ai-config profile <list|show|set> ...");
|
|
2918
3579
|
return 1;
|
|
2919
3580
|
}
|
|
3581
|
+
case "bootstrap": {
|
|
3582
|
+
const subcommand = args[1];
|
|
3583
|
+
if (subcommand === "list" || subcommand === "show") {
|
|
3584
|
+
try {
|
|
3585
|
+
console.log(formatBootstrapInventory(resolveBootstrapState().state));
|
|
3586
|
+
} catch (e) {
|
|
3587
|
+
console.error(`Error: ${e.message}`);
|
|
3588
|
+
return 1;
|
|
3589
|
+
}
|
|
3590
|
+
return 0;
|
|
3591
|
+
}
|
|
3592
|
+
if (subcommand === "enable" || subcommand === "disable") {
|
|
3593
|
+
const ruleId = args[2];
|
|
3594
|
+
if (!ruleId) {
|
|
3595
|
+
console.error("Usage: massa-ai-config bootstrap <enable|disable> <rule-id> [--target <dir> --yes] [--dry-run]");
|
|
3596
|
+
return 1;
|
|
3597
|
+
}
|
|
3598
|
+
try {
|
|
3599
|
+
assertKnownRuleId(ruleId);
|
|
3600
|
+
} catch (e) {
|
|
3601
|
+
console.error(`Error: ${e.message}`);
|
|
3602
|
+
return 1;
|
|
3603
|
+
}
|
|
3604
|
+
const targetOpt = typeof options.target === "string" ? options.target : undefined;
|
|
3605
|
+
const targetHome = targetOpt === undefined ? os8.homedir() : path15.resolve(targetOpt);
|
|
3606
|
+
if (targetHome !== os8.homedir() && options.yes !== true) {
|
|
3607
|
+
console.error(`Error: --target ${targetHome} is not your home (${os8.homedir()}) \u2014 pass --yes to confirm writing there`);
|
|
3608
|
+
return 1;
|
|
3609
|
+
}
|
|
3610
|
+
const dryRun = options["dry-run"] === true;
|
|
3611
|
+
try {
|
|
3612
|
+
if (dryRun) {
|
|
3613
|
+
console.log(`bootstrap ${subcommand} ${ruleId}: dry run \u2014 ${getConfigPath()} was not written`);
|
|
3614
|
+
} else {
|
|
3615
|
+
setBootstrapRuleEnabled(ruleId, subcommand === "enable");
|
|
3616
|
+
}
|
|
3617
|
+
const stateFile = bootstrapStateFilePath(targetHome);
|
|
3618
|
+
if (stateFile !== getConfigPath()) {
|
|
3619
|
+
console.error(`Warning: the rule state is persisted to ${getConfigPath()}, but --target renders from ${stateFile} \u2014 set XDG_CONFIG_HOME to move the persisted state`);
|
|
3620
|
+
}
|
|
3621
|
+
const repoRoot = findRepoRootWithMarker(__dirname2, GENERATOR_MARKER, GENERATOR_MARKER_MAX_LEVELS);
|
|
3622
|
+
const report = applyBootstrapState({
|
|
3623
|
+
targetHome,
|
|
3624
|
+
dryRun,
|
|
3625
|
+
sourcePath: repoRoot === null ? undefined : path15.join(repoRoot, "skills", "AGENTS.md")
|
|
3626
|
+
});
|
|
3627
|
+
console.log(formatBootstrapReport(report));
|
|
3628
|
+
return bootstrapReportSucceeded(report) ? 0 : 1;
|
|
3629
|
+
} catch (e) {
|
|
3630
|
+
console.error(`Error: ${e.message}`);
|
|
3631
|
+
return 1;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
console.error("Usage: massa-ai-config bootstrap <list|show|enable|disable> ...");
|
|
3635
|
+
return 1;
|
|
3636
|
+
}
|
|
2920
3637
|
default:
|
|
2921
3638
|
console.error(`Unknown command: ${command}`);
|
|
2922
3639
|
help();
|
package/dist/index.js
CHANGED
|
@@ -600,15 +600,18 @@ var init_massa_ai_config = __esm(() => {
|
|
|
600
600
|
security: {
|
|
601
601
|
corsOrigins: []
|
|
602
602
|
},
|
|
603
|
-
scheduler: DEFAULT_SCHEDULER_CONFIG
|
|
603
|
+
scheduler: DEFAULT_SCHEDULER_CONFIG,
|
|
604
|
+
bootstrap: { rules: {} }
|
|
604
605
|
};
|
|
605
606
|
});
|
|
606
607
|
|
|
607
608
|
// ../../packages/shared/dist/config/config-loader.js
|
|
608
609
|
var exports_config_loader = {};
|
|
609
610
|
__export(exports_config_loader, {
|
|
611
|
+
writeRawConfig: () => writeRawConfig,
|
|
610
612
|
writeFileAtomically: () => writeFileAtomically,
|
|
611
613
|
saveConfig: () => saveConfig,
|
|
614
|
+
readRawConfigStrict: () => readRawConfigStrict,
|
|
612
615
|
migrateDataDirOnce: () => migrateDataDirOnce,
|
|
613
616
|
mergeSchedulerSection: () => mergeSchedulerSection,
|
|
614
617
|
loadRawUserConfig: () => loadRawUserConfig,
|
|
@@ -619,12 +622,23 @@ __export(exports_config_loader, {
|
|
|
619
622
|
getConfigForEnv: () => getConfigForEnv,
|
|
620
623
|
getConfigDir: () => getConfigDir,
|
|
621
624
|
configExists: () => configExists,
|
|
622
|
-
__resetMigrationForTests: () => __resetMigrationForTests
|
|
625
|
+
__resetMigrationForTests: () => __resetMigrationForTests,
|
|
626
|
+
ConfigWriteConflictError: () => ConfigWriteConflictError,
|
|
627
|
+
ConfigParseError: () => ConfigParseError
|
|
623
628
|
});
|
|
624
629
|
import fs from "fs";
|
|
625
630
|
import path3 from "path";
|
|
626
631
|
import os2 from "os";
|
|
627
632
|
import crypto from "crypto";
|
|
633
|
+
function readConfigFileOrEmpty() {
|
|
634
|
+
try {
|
|
635
|
+
return fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (error?.code === "ENOENT")
|
|
638
|
+
return "";
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
628
642
|
function getConfigDir() {
|
|
629
643
|
return CONFIG_DIR;
|
|
630
644
|
}
|
|
@@ -691,6 +705,21 @@ function loadRawUserConfig() {
|
|
|
691
705
|
return {};
|
|
692
706
|
}
|
|
693
707
|
}
|
|
708
|
+
function readRawConfigStrict() {
|
|
709
|
+
const raw = readConfigFileOrEmpty();
|
|
710
|
+
if (raw === "")
|
|
711
|
+
return {};
|
|
712
|
+
let parsed;
|
|
713
|
+
try {
|
|
714
|
+
parsed = JSON.parse(raw);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
throw new ConfigParseError(CONFIG_FILE, error);
|
|
717
|
+
}
|
|
718
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
719
|
+
throw new ConfigParseError(CONFIG_FILE, new Error("parsed value is not a JSON object"));
|
|
720
|
+
}
|
|
721
|
+
return parsed;
|
|
722
|
+
}
|
|
694
723
|
function loadConfigSafe() {
|
|
695
724
|
try {
|
|
696
725
|
return loadConfig();
|
|
@@ -745,6 +774,43 @@ function writeFileAtomically(targetPath, content) {
|
|
|
745
774
|
function saveConfig(config) {
|
|
746
775
|
writeFileAtomically(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
747
776
|
}
|
|
777
|
+
function writeRawConfig(doc, opts) {
|
|
778
|
+
const onDiskAtStart = readConfigFileOrEmpty();
|
|
779
|
+
if (onDiskAtStart === opts.expectedBytes) {
|
|
780
|
+
writeFileAtomically(CONFIG_FILE, JSON.stringify(doc, null, 2));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
let original;
|
|
784
|
+
let current;
|
|
785
|
+
try {
|
|
786
|
+
original = opts.expectedBytes === "" ? {} : JSON.parse(opts.expectedBytes);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
throw new ConfigParseError(`${CONFIG_FILE} (caller-supplied expectedBytes)`, error);
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
current = onDiskAtStart === "" ? {} : JSON.parse(onDiskAtStart);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
throw new ConfigParseError(CONFIG_FILE, error);
|
|
794
|
+
}
|
|
795
|
+
const reapplied = { ...current };
|
|
796
|
+
const touchedKeys = new Set([...Object.keys(original), ...Object.keys(doc)]);
|
|
797
|
+
for (const key of touchedKeys) {
|
|
798
|
+
const before = JSON.stringify(original[key]);
|
|
799
|
+
const after = JSON.stringify(doc[key]);
|
|
800
|
+
if (before === after)
|
|
801
|
+
continue;
|
|
802
|
+
if (Object.prototype.hasOwnProperty.call(doc, key)) {
|
|
803
|
+
reapplied[key] = doc[key];
|
|
804
|
+
} else {
|
|
805
|
+
delete reapplied[key];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const onDiskImmediatelyBeforeWrite = readConfigFileOrEmpty();
|
|
809
|
+
if (onDiskImmediatelyBeforeWrite !== onDiskAtStart) {
|
|
810
|
+
throw new ConfigWriteConflictError(CONFIG_FILE);
|
|
811
|
+
}
|
|
812
|
+
writeFileAtomically(CONFIG_FILE, JSON.stringify(reapplied, null, 2));
|
|
813
|
+
}
|
|
748
814
|
function initConfig() {
|
|
749
815
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
750
816
|
saveConfig(defaultMassaAiConfig);
|
|
@@ -771,12 +837,25 @@ function getConfigForEnv() {
|
|
|
771
837
|
env.ENABLE_METRICS = String(config.logging.enableMetrics);
|
|
772
838
|
return env;
|
|
773
839
|
}
|
|
774
|
-
var CONFIG_DIR, CONFIG_FILE, migrationAttempted = false, tempFileCounter = 0;
|
|
840
|
+
var CONFIG_DIR, CONFIG_FILE, ConfigParseError, ConfigWriteConflictError, migrationAttempted = false, tempFileCounter = 0;
|
|
775
841
|
var init_config_loader = __esm(() => {
|
|
776
842
|
init_massa_ai_config();
|
|
777
843
|
init_xdg();
|
|
778
844
|
CONFIG_DIR = configDir("massa-ai");
|
|
779
845
|
CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
|
|
846
|
+
ConfigParseError = class ConfigParseError extends Error {
|
|
847
|
+
constructor(filePath, cause) {
|
|
848
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
849
|
+
super(`Failed to parse ${filePath}: ${reason}`);
|
|
850
|
+
this.name = "ConfigParseError";
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
ConfigWriteConflictError = class ConfigWriteConflictError extends Error {
|
|
854
|
+
constructor(filePath) {
|
|
855
|
+
super(`${filePath} changed on disk twice while writing \u2014 refusing to overwrite a ` + `concurrent update. Re-read the file and retry.`);
|
|
856
|
+
this.name = "ConfigWriteConflictError";
|
|
857
|
+
}
|
|
858
|
+
};
|
|
780
859
|
});
|
|
781
860
|
|
|
782
861
|
// ../../packages/shared/dist/env.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massa-ai/opencode-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.0",
|
|
4
4
|
"description": "massa-ai plugin for OpenCode - Semantic code search, memory, and context compression",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@opencode-ai/plugin": "^1.2.15",
|
|
26
26
|
"@opencode-ai/sdk": "^1.2.15",
|
|
27
|
-
"@massa-ai/core": "^1.
|
|
28
|
-
"@massa-ai/shared": "^1.
|
|
27
|
+
"@massa-ai/core": "^1.56.0",
|
|
28
|
+
"@massa-ai/shared": "^1.56.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.10.5",
|