@hasna/instructions 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/dist/cli/index.js +3128 -3063
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1685 -1452
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/config-agents.d.ts +6 -0
- package/dist/lib/config-agents.d.ts.map +1 -0
- package/dist/lib/global-agent-rules-standard.d.ts +6 -0
- package/dist/lib/global-agent-rules-standard.d.ts.map +1 -0
- package/dist/lib/global-agent-rules-standard.test.d.ts +2 -0
- package/dist/lib/global-agent-rules-standard.test.d.ts.map +1 -0
- package/dist/lib/session-render.d.ts +9 -4
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/mcp/index.js +95 -22
- package/dist/server/index.js +38 -58
- package/dist/status.d.ts +3 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -14,11 +14,11 @@ var CONFIG_CATEGORIES = [
|
|
|
14
14
|
var CONFIG_AGENTS = [
|
|
15
15
|
"claude",
|
|
16
16
|
"codex",
|
|
17
|
-
"gemini",
|
|
18
17
|
"opencode",
|
|
19
18
|
"cursor",
|
|
20
19
|
"codewith",
|
|
21
20
|
"aicopilot",
|
|
21
|
+
"antigravity",
|
|
22
22
|
"zsh",
|
|
23
23
|
"git",
|
|
24
24
|
"npm",
|
|
@@ -1067,1497 +1067,1637 @@ function resolveConfigStore(env = process.env) {
|
|
|
1067
1067
|
return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
|
|
1068
1068
|
}
|
|
1069
1069
|
// src/status.ts
|
|
1070
|
-
import { existsSync as
|
|
1070
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
1071
1071
|
|
|
1072
1072
|
// src/lib/apply.ts
|
|
1073
|
-
import { existsSync as
|
|
1074
|
-
import { basename as
|
|
1075
|
-
import { homedir as
|
|
1073
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, writeFileSync } from "fs";
|
|
1074
|
+
import { basename as basename3, dirname as dirname2, resolve as resolve2 } from "path";
|
|
1075
|
+
import { homedir as homedir3 } from "os";
|
|
1076
1076
|
|
|
1077
|
-
// src/lib/
|
|
1078
|
-
|
|
1077
|
+
// src/lib/config-agents.ts
|
|
1078
|
+
var DEPRECATED_CONFIG_AGENTS = ["gemini"];
|
|
1079
|
+
var ACTIVE_CONFIG_AGENT_SET = new Set(CONFIG_AGENTS);
|
|
1080
|
+
var DEPRECATED_CONFIG_AGENT_SET = new Set(DEPRECATED_CONFIG_AGENTS);
|
|
1081
|
+
function isDeprecatedConfigAgent(agent) {
|
|
1082
|
+
return !!agent && DEPRECATED_CONFIG_AGENT_SET.has(agent);
|
|
1083
|
+
}
|
|
1084
|
+
function isSupportedConfigAgent(agent) {
|
|
1085
|
+
return !!agent && ACTIVE_CONFIG_AGENT_SET.has(agent) && !isDeprecatedConfigAgent(agent);
|
|
1086
|
+
}
|
|
1087
|
+
function isRetiredOrUnsupportedConfigAgent(agent) {
|
|
1088
|
+
return !!agent && !isSupportedConfigAgent(agent);
|
|
1089
|
+
}
|
|
1090
|
+
function retiredOrUnsupportedAgentReason(agent) {
|
|
1091
|
+
if (!agent)
|
|
1092
|
+
return "missing agent";
|
|
1093
|
+
return isDeprecatedConfigAgent(agent) ? `deprecated agent: ${agent}` : `unsupported agent: ${agent}`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/lib/session-render.ts
|
|
1097
|
+
import { createHash } from "crypto";
|
|
1098
|
+
import { existsSync as existsSync3, readFileSync, realpathSync, statSync } from "fs";
|
|
1099
|
+
import { homedir as homedir2 } from "os";
|
|
1100
|
+
import { basename, dirname, extname, isAbsolute, join as join3, parse, posix, relative, resolve } from "path";
|
|
1101
|
+
var CODEWITH_NATIVE_IMPORTS_ENV = "HASNA_CONFIGS_CODEWITH_NATIVE_IMPORTS";
|
|
1102
|
+
var SESSION_RENDER_MANAGED_MARKER = "Managed by @hasna/configs session render";
|
|
1103
|
+
var SESSION_RENDER_SCHEMA = "hasna.configs.session-render/v1";
|
|
1104
|
+
var RAW_STORE_ROOT_ENV = "HASNA_CONFIGS_HOME";
|
|
1105
|
+
var ANTIGRAVITY_RULE_FILE_CHAR_LIMIT = 12000;
|
|
1106
|
+
var SESSION_RENDER_TOOLS = [
|
|
1107
|
+
"claude",
|
|
1108
|
+
"codex",
|
|
1109
|
+
"cursor",
|
|
1110
|
+
"opencode",
|
|
1111
|
+
"codewith",
|
|
1112
|
+
"aicopilot",
|
|
1113
|
+
"antigravity"
|
|
1114
|
+
];
|
|
1115
|
+
var SESSION_INSTRUCTION_LAYERS = [
|
|
1116
|
+
"global",
|
|
1117
|
+
"tool",
|
|
1118
|
+
"account",
|
|
1119
|
+
"machine",
|
|
1120
|
+
"division",
|
|
1121
|
+
"workspace",
|
|
1122
|
+
"repo",
|
|
1123
|
+
"path",
|
|
1124
|
+
"agent",
|
|
1125
|
+
"session",
|
|
1126
|
+
"local"
|
|
1127
|
+
];
|
|
1128
|
+
var CODEWITH_FLATTENED_ADAPTER = {
|
|
1129
|
+
tool: "codewith",
|
|
1130
|
+
mode: "flattened-markdown",
|
|
1131
|
+
indexFile: "CODEWITH.md",
|
|
1132
|
+
managedDir: ".hasna/instructions",
|
|
1133
|
+
envVar: "CODEWITH_HOME",
|
|
1134
|
+
nativeImports: false,
|
|
1135
|
+
description: "Codewith CODEWITH.md flattened until native @ imports are implemented in Codewith."
|
|
1136
|
+
};
|
|
1137
|
+
var CODEWITH_NATIVE_ADAPTER = {
|
|
1138
|
+
tool: "codewith",
|
|
1139
|
+
mode: "native-imports",
|
|
1140
|
+
indexFile: "CODEWITH.md",
|
|
1141
|
+
managedDir: ".hasna/instructions",
|
|
1142
|
+
envVar: "CODEWITH_HOME",
|
|
1143
|
+
nativeImports: true,
|
|
1144
|
+
description: "Codewith CODEWITH.md with gated @ imports into managed fragments."
|
|
1145
|
+
};
|
|
1146
|
+
var SESSION_TOOL_ADAPTERS = {
|
|
1147
|
+
claude: {
|
|
1148
|
+
tool: "claude",
|
|
1149
|
+
mode: "native-imports",
|
|
1150
|
+
indexFile: "CLAUDE.md",
|
|
1151
|
+
managedDir: ".hasna/instructions",
|
|
1152
|
+
envVar: "CLAUDE_CONFIG_DIR",
|
|
1153
|
+
nativeImports: true,
|
|
1154
|
+
description: "Claude Code CLAUDE.md with @ imports into managed fragments."
|
|
1155
|
+
},
|
|
1156
|
+
codex: {
|
|
1157
|
+
tool: "codex",
|
|
1158
|
+
mode: "flattened-markdown",
|
|
1159
|
+
indexFile: "AGENTS.md",
|
|
1160
|
+
managedDir: ".hasna/instructions",
|
|
1161
|
+
envVar: "CODEX_HOME",
|
|
1162
|
+
nativeImports: false,
|
|
1163
|
+
description: "Codex AGENTS.md flattened instruction file."
|
|
1164
|
+
},
|
|
1165
|
+
cursor: {
|
|
1166
|
+
tool: "cursor",
|
|
1167
|
+
mode: "cursor-mdc",
|
|
1168
|
+
managedDir: ".cursor/rules",
|
|
1169
|
+
nativeImports: false,
|
|
1170
|
+
description: "Cursor project rule files in .cursor/rules/*.mdc."
|
|
1171
|
+
},
|
|
1172
|
+
opencode: {
|
|
1173
|
+
tool: "opencode",
|
|
1174
|
+
mode: "opencode-instructions",
|
|
1175
|
+
indexFile: "AGENTS.md",
|
|
1176
|
+
configFile: "opencode.json",
|
|
1177
|
+
managedDir: ".hasna/instructions",
|
|
1178
|
+
envVar: "OPENCODE_CONFIG_DIR",
|
|
1179
|
+
nativeImports: false,
|
|
1180
|
+
description: "OpenCode AGENTS.md plus opencode.json instructions pointing at managed fragments."
|
|
1181
|
+
},
|
|
1182
|
+
aicopilot: {
|
|
1183
|
+
tool: "aicopilot",
|
|
1184
|
+
mode: "flattened-markdown",
|
|
1185
|
+
indexFile: "AICOPILOT.md",
|
|
1186
|
+
managedDir: ".hasna/instructions",
|
|
1187
|
+
envVar: "AICOPILOT_CONFIG_DIR",
|
|
1188
|
+
nativeImports: false,
|
|
1189
|
+
description: "AI Copilot AICOPILOT.md flattened instruction file."
|
|
1190
|
+
},
|
|
1191
|
+
antigravity: {
|
|
1192
|
+
tool: "antigravity",
|
|
1193
|
+
mode: "antigravity-rules",
|
|
1194
|
+
managedDir: ".agents/rules",
|
|
1195
|
+
nativeImports: false,
|
|
1196
|
+
description: "Google Antigravity project rules in .agents/rules/*.md."
|
|
1197
|
+
},
|
|
1198
|
+
codewith: CODEWITH_FLATTENED_ADAPTER
|
|
1199
|
+
};
|
|
1200
|
+
var SESSION_LAYER_RANK = {
|
|
1201
|
+
global: 10,
|
|
1202
|
+
tool: 20,
|
|
1203
|
+
account: 30,
|
|
1204
|
+
machine: 40,
|
|
1205
|
+
division: 50,
|
|
1206
|
+
workspace: 60,
|
|
1207
|
+
repo: 70,
|
|
1208
|
+
path: 80,
|
|
1209
|
+
agent: 90,
|
|
1210
|
+
session: 100,
|
|
1211
|
+
local: 110
|
|
1212
|
+
};
|
|
1213
|
+
function normalizeSessionInstructionLayer(value) {
|
|
1214
|
+
if (value === "provider")
|
|
1215
|
+
return "tool";
|
|
1216
|
+
if (value === "identity")
|
|
1217
|
+
return "agent";
|
|
1218
|
+
if (value === "project")
|
|
1219
|
+
return "repo";
|
|
1220
|
+
if (value === "global" || value === "tool" || value === "account" || value === "machine" || value === "division" || value === "workspace" || value === "repo" || value === "path" || value === "agent" || value === "session" || value === "local")
|
|
1221
|
+
return value;
|
|
1222
|
+
throw new Error(`Invalid session instruction layer: ${String(value)}`);
|
|
1223
|
+
}
|
|
1079
1224
|
function ensureTrailingNewline(content) {
|
|
1080
1225
|
return content.endsWith(`
|
|
1081
1226
|
`) ? content : `${content}
|
|
1082
1227
|
`;
|
|
1083
1228
|
}
|
|
1229
|
+
function sha256(content) {
|
|
1230
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1231
|
+
}
|
|
1232
|
+
function fingerprint(value) {
|
|
1233
|
+
return sha256(JSON.stringify(value));
|
|
1234
|
+
}
|
|
1235
|
+
function slug(value) {
|
|
1236
|
+
const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1237
|
+
return s || "instruction";
|
|
1238
|
+
}
|
|
1084
1239
|
function yamlQuote(value) {
|
|
1085
1240
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
1086
1241
|
}
|
|
1087
|
-
function
|
|
1088
|
-
|
|
1089
|
-
return m ? m[1].length : null;
|
|
1242
|
+
function getRawStoreRoot() {
|
|
1243
|
+
return resolve(process.env[RAW_STORE_ROOT_ENV] || join3(process.env["HOME"] || homedir2(), ".hasna", "configs"));
|
|
1090
1244
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1245
|
+
function defaultTargetHome(tool, profile, sessionId) {
|
|
1246
|
+
return join3(getRawStoreRoot(), "sessions", tool, slug(profile), slug(sessionId || "latest"));
|
|
1247
|
+
}
|
|
1248
|
+
function joinTarget(targetHome, relativePath) {
|
|
1249
|
+
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
1250
|
+
const safeRelativePath = assertSafeRelativePath(relativePath);
|
|
1251
|
+
return join3(safeTargetHome, ...safeRelativePath.split("/"));
|
|
1252
|
+
}
|
|
1253
|
+
function makeFile(targetHome, relativePath, role, content, sourceIds) {
|
|
1254
|
+
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
1255
|
+
const safeRelativePath = assertSafeRelativePath(relativePath);
|
|
1256
|
+
const normalizedContent = ensureTrailingNewline(content);
|
|
1257
|
+
return {
|
|
1258
|
+
path: joinTarget(safeTargetHome, safeRelativePath),
|
|
1259
|
+
relativePath: safeRelativePath,
|
|
1260
|
+
role,
|
|
1261
|
+
content: normalizedContent,
|
|
1262
|
+
sha256: sha256(normalizedContent),
|
|
1263
|
+
sourceIds
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function normalizeSources(sources, tool, allowEmptySources) {
|
|
1267
|
+
const ordered = sources.map((source, index) => {
|
|
1268
|
+
if (!source.id.trim())
|
|
1269
|
+
throw new Error("Session instruction source id is required.");
|
|
1270
|
+
const content = filterProviderOnlyBlocks(source.content ?? "", tool);
|
|
1271
|
+
const normalized = {
|
|
1272
|
+
...source,
|
|
1273
|
+
content,
|
|
1274
|
+
normalizedId: slug(source.id),
|
|
1275
|
+
resolvedLabel: source.label ?? source.id,
|
|
1276
|
+
resolvedLayer: source.layer === undefined ? "agent" : normalizeSessionInstructionLayer(source.layer),
|
|
1277
|
+
resolvedMerge: source.merge ?? "append",
|
|
1278
|
+
resolvedOrder: source.order ?? index,
|
|
1279
|
+
resolvedRules: normalizeInstructionRules(source, tool)
|
|
1280
|
+
};
|
|
1281
|
+
const hasPathReferences = (normalized.sourcePaths ?? []).length > 0;
|
|
1282
|
+
if (!allowEmptySources && !normalized.content.trim() && normalized.resolvedRules.length === 0 && !hasPathReferences) {
|
|
1283
|
+
throw new Error(`Session instruction source "${source.id}" is empty. Pass --allow-empty-sources only for explicit empty renders.`);
|
|
1101
1284
|
}
|
|
1102
|
-
|
|
1285
|
+
return normalized;
|
|
1286
|
+
}).sort((a, b) => SESSION_LAYER_RANK[a.resolvedLayer] - SESSION_LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id));
|
|
1287
|
+
rejectDuplicateSourceSlugs(ordered);
|
|
1288
|
+
rejectDuplicateRulePaths(ordered);
|
|
1289
|
+
return ordered;
|
|
1290
|
+
}
|
|
1291
|
+
function filterProviderOnlyBlocks(content, tool) {
|
|
1292
|
+
const lines = content.split(/\r?\n/);
|
|
1293
|
+
const output = [];
|
|
1294
|
+
let activeProviders = null;
|
|
1295
|
+
for (const line of lines) {
|
|
1296
|
+
const start = line.match(/^\s*<!--\s*@hasna-provider:\s*([^>]+?)\s*-->\s*$/i);
|
|
1297
|
+
if (start) {
|
|
1298
|
+
if (activeProviders)
|
|
1299
|
+
throw new Error("Nested provider-only instruction blocks are not supported.");
|
|
1300
|
+
activeProviders = start[1].split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
1103
1301
|
continue;
|
|
1104
|
-
|
|
1105
|
-
|
|
1302
|
+
}
|
|
1303
|
+
if (/^\s*<!--\s*@hasna-end-provider\s*-->\s*$/i.test(line)) {
|
|
1304
|
+
if (!activeProviders)
|
|
1305
|
+
throw new Error("Provider-only instruction block end marker without start marker.");
|
|
1306
|
+
activeProviders = null;
|
|
1106
1307
|
continue;
|
|
1107
1308
|
}
|
|
1108
|
-
|
|
1309
|
+
if (!activeProviders || activeProviders.includes(tool) || activeProviders.includes("all") || activeProviders.includes("generic")) {
|
|
1310
|
+
output.push(line);
|
|
1311
|
+
}
|
|
1109
1312
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
`)
|
|
1114
|
-
}
|
|
1115
|
-
function isClaudeRuleConfig(source, candidate) {
|
|
1116
|
-
if (candidate.id === source.id)
|
|
1117
|
-
return false;
|
|
1118
|
-
if (candidate.agent !== "claude" || candidate.category !== "rules")
|
|
1119
|
-
return false;
|
|
1120
|
-
return !!candidate.target_path?.includes("/rules/");
|
|
1121
|
-
}
|
|
1122
|
-
function ruleLabel(config) {
|
|
1123
|
-
const file = config.target_path ? basename(config.target_path) : config.name;
|
|
1124
|
-
return file.replace(/\.(md|mdc|markdown)$/i, "");
|
|
1313
|
+
if (activeProviders)
|
|
1314
|
+
throw new Error("Provider-only instruction block was not closed.");
|
|
1315
|
+
return output.join(`
|
|
1316
|
+
`);
|
|
1125
1317
|
}
|
|
1126
|
-
function
|
|
1127
|
-
|
|
1318
|
+
function composeSources(sources) {
|
|
1319
|
+
let start = -1;
|
|
1320
|
+
for (let i = 0;i < sources.length; i++) {
|
|
1321
|
+
if (sources[i].resolvedMerge === "replace")
|
|
1322
|
+
start = i;
|
|
1323
|
+
}
|
|
1324
|
+
if (start < 0)
|
|
1325
|
+
return sources;
|
|
1326
|
+
const protectedSources = sources.slice(0, start).filter((source) => source.nonOverridable);
|
|
1327
|
+
return [...protectedSources, ...sources.slice(start)];
|
|
1128
1328
|
}
|
|
1129
|
-
function
|
|
1130
|
-
const parts = [
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (
|
|
1135
|
-
parts.push(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1329
|
+
function sectionForSource(source) {
|
|
1330
|
+
const parts = [
|
|
1331
|
+
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1332
|
+
`# ${source.resolvedLabel}`
|
|
1333
|
+
];
|
|
1334
|
+
if (source.path)
|
|
1335
|
+
parts.push(`Source: ${source.path}`);
|
|
1336
|
+
if (source.sourcePaths && source.sourcePaths.length > 0) {
|
|
1337
|
+
parts.push([
|
|
1338
|
+
"Source paths:",
|
|
1339
|
+
...source.sourcePaths.map((sourcePath) => {
|
|
1340
|
+
const flags = [
|
|
1341
|
+
sourcePath.editable ? "editable" : null,
|
|
1342
|
+
sourcePath.required ? "required" : null,
|
|
1343
|
+
sourcePath.hash ? sourcePath.hash : null
|
|
1344
|
+
].filter(Boolean);
|
|
1345
|
+
return `- ${sourcePath.path}${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
|
|
1346
|
+
})
|
|
1347
|
+
].join(`
|
|
1348
|
+
`));
|
|
1140
1349
|
}
|
|
1141
|
-
|
|
1350
|
+
if (source.owner)
|
|
1351
|
+
parts.push(`Owner: ${source.owner.kind}:${source.owner.id}`);
|
|
1352
|
+
const content = source.content.trim();
|
|
1353
|
+
if (content)
|
|
1354
|
+
parts.push(content);
|
|
1355
|
+
return parts.join(`
|
|
1142
1356
|
|
|
1143
|
-
`)
|
|
1144
|
-
}
|
|
1145
|
-
function buildCodexAgentsMd(source, context = {}) {
|
|
1146
|
-
return flattenWithRules(source, context);
|
|
1357
|
+
`);
|
|
1147
1358
|
}
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1359
|
+
function sectionForRule(source, rule) {
|
|
1360
|
+
const parts = [
|
|
1361
|
+
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1362
|
+
`# ${rule.resolvedLabel}`
|
|
1363
|
+
];
|
|
1364
|
+
if (source.path)
|
|
1365
|
+
parts.push(`Source: ${source.path}`);
|
|
1366
|
+
if (rule.path)
|
|
1367
|
+
parts.push(`Rule path: ${rule.path}`);
|
|
1368
|
+
const content = rule.content.trim();
|
|
1369
|
+
if (content)
|
|
1370
|
+
parts.push(content);
|
|
1371
|
+
return parts.join(`
|
|
1372
|
+
|
|
1373
|
+
`);
|
|
1150
1374
|
}
|
|
1151
|
-
function
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1375
|
+
function fragmentPath(adapter, index, source) {
|
|
1376
|
+
const n = String(index + 1).padStart(2, "0");
|
|
1377
|
+
return posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
|
|
1378
|
+
}
|
|
1379
|
+
function ruleFragmentPath(adapter, source, rule) {
|
|
1380
|
+
return posix.join(adapter.managedDir, "rules", source.normalizedId, rule.resolvedPath);
|
|
1381
|
+
}
|
|
1382
|
+
function importPath(indexRelativePath, fragmentRelativePath) {
|
|
1383
|
+
const relative2 = posix.relative(posix.dirname(indexRelativePath), fragmentRelativePath);
|
|
1384
|
+
if (relative2.startsWith("./") || relative2.startsWith("../"))
|
|
1385
|
+
return relative2;
|
|
1386
|
+
return `./${relative2}`;
|
|
1387
|
+
}
|
|
1388
|
+
function indexHeader(tool, profile) {
|
|
1389
|
+
return [
|
|
1390
|
+
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1391
|
+
`# ${tool} session instructions`,
|
|
1160
1392
|
"",
|
|
1161
|
-
|
|
1393
|
+
`Profile: ${profile}`
|
|
1162
1394
|
].join(`
|
|
1163
|
-
`)
|
|
1395
|
+
`);
|
|
1164
1396
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1397
|
+
function buildNativeImportFiles(targetHome, adapter, profile, sources) {
|
|
1398
|
+
const indexFile = adapter.indexFile;
|
|
1399
|
+
const fragments = sources.flatMap((source, index2) => [
|
|
1400
|
+
makeFile(targetHome, fragmentPath(adapter, index2, source), "fragment", sectionForSource(source), [source.id]),
|
|
1401
|
+
...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
|
|
1402
|
+
]);
|
|
1403
|
+
const imports = fragments.map((file) => `@${importPath(indexFile, file.relativePath)}`);
|
|
1404
|
+
const index = makeFile(targetHome, indexFile, "index", [indexHeader(adapter.tool, profile), ...imports].join(`
|
|
1405
|
+
`), sources.map((source) => source.id));
|
|
1406
|
+
return [index, ...fragments];
|
|
1169
1407
|
}
|
|
1170
|
-
function
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1408
|
+
function buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources) {
|
|
1409
|
+
const content = [
|
|
1410
|
+
indexHeader(adapter.tool, profile),
|
|
1411
|
+
...sources.flatMap((source) => [
|
|
1412
|
+
sectionForSource(source),
|
|
1413
|
+
...source.resolvedRules.map((rule) => sectionForRule(source, rule))
|
|
1414
|
+
])
|
|
1415
|
+
].join(`
|
|
1416
|
+
|
|
1417
|
+
`);
|
|
1418
|
+
return [
|
|
1419
|
+
makeFile(targetHome, adapter.indexFile, "index", content, [
|
|
1420
|
+
...sources.map((source) => source.id),
|
|
1421
|
+
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
1422
|
+
])
|
|
1423
|
+
];
|
|
1424
|
+
}
|
|
1425
|
+
function buildCursorRuleFiles(targetHome, adapter, sources) {
|
|
1426
|
+
return sources.flatMap((source, index) => {
|
|
1427
|
+
const n = String(index + 1).padStart(2, "0");
|
|
1428
|
+
const stem = `${n}-${source.normalizedId}`;
|
|
1429
|
+
const relativePath = posix.join(adapter.managedDir, `${stem}.mdc`);
|
|
1430
|
+
const description = `${source.resolvedLabel} (${source.resolvedLayer})`;
|
|
1431
|
+
const content = [
|
|
1432
|
+
"---",
|
|
1433
|
+
`description: ${yamlQuote(description)}`,
|
|
1434
|
+
'globs: ["**/*"]',
|
|
1435
|
+
"alwaysApply: true",
|
|
1436
|
+
"---",
|
|
1437
|
+
"",
|
|
1438
|
+
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1439
|
+
"",
|
|
1440
|
+
source.content.trim()
|
|
1441
|
+
].join(`
|
|
1442
|
+
`);
|
|
1443
|
+
const sourceFile = makeFile(targetHome, relativePath, "rule", content, [source.id]);
|
|
1444
|
+
const ruleFiles = source.resolvedRules.map((rule) => {
|
|
1445
|
+
const ruleStem = `${n}-${source.normalizedId}-${rule.normalizedId}`;
|
|
1446
|
+
const ruleRelativePath = posix.join(adapter.managedDir, `${ruleStem}.mdc`);
|
|
1447
|
+
const ruleDescription = `${rule.resolvedLabel} (${source.resolvedLayer})`;
|
|
1448
|
+
const ruleContent = [
|
|
1449
|
+
"---",
|
|
1450
|
+
`description: ${yamlQuote(ruleDescription)}`,
|
|
1451
|
+
`globs: ${JSON.stringify(rule.globs && rule.globs.length > 0 ? rule.globs : ["**/*"])}`,
|
|
1452
|
+
"alwaysApply: true",
|
|
1453
|
+
"---",
|
|
1454
|
+
"",
|
|
1455
|
+
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1456
|
+
"",
|
|
1457
|
+
rule.content.trim()
|
|
1458
|
+
].join(`
|
|
1459
|
+
`);
|
|
1460
|
+
return makeFile(targetHome, ruleRelativePath, "rule", ruleContent, [source.id, rule.id]);
|
|
1461
|
+
});
|
|
1462
|
+
return [sourceFile, ...ruleFiles];
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
function buildOpenCodeFiles(targetHome, adapter, profile, sources) {
|
|
1466
|
+
const fragments = sources.flatMap((source, index) => [
|
|
1467
|
+
makeFile(targetHome, fragmentPath(adapter, index, source), "fragment", sectionForSource(source), [source.id]),
|
|
1468
|
+
...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
|
|
1469
|
+
]);
|
|
1470
|
+
const flattenedIndex = makeFile(targetHome, adapter.indexFile, "index", [
|
|
1471
|
+
indexHeader(adapter.tool, profile),
|
|
1472
|
+
...sources.flatMap((source) => [
|
|
1473
|
+
sectionForSource(source),
|
|
1474
|
+
...source.resolvedRules.map((rule) => sectionForRule(source, rule))
|
|
1475
|
+
])
|
|
1476
|
+
].join(`
|
|
1477
|
+
|
|
1478
|
+
`), [
|
|
1479
|
+
...sources.map((source) => source.id),
|
|
1480
|
+
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
1481
|
+
]);
|
|
1482
|
+
const config = {
|
|
1483
|
+
$schema: "https://opencode.ai/config.json",
|
|
1484
|
+
instructions: fragments.map((file) => file.relativePath)
|
|
1485
|
+
};
|
|
1486
|
+
return [
|
|
1487
|
+
flattenedIndex,
|
|
1488
|
+
makeFile(targetHome, adapter.configFile, "config", JSON.stringify(config, null, 2), sources.map((source) => source.id)),
|
|
1489
|
+
...fragments
|
|
1490
|
+
];
|
|
1491
|
+
}
|
|
1492
|
+
function buildAntigravityRuleFiles(targetHome, adapter, sources) {
|
|
1493
|
+
return sources.flatMap((source, index) => {
|
|
1494
|
+
const n = String(index + 1).padStart(2, "0");
|
|
1495
|
+
const sourcePath = posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
|
|
1496
|
+
const sourceFile = makeAntigravityRuleFile(targetHome, sourcePath, sectionForSource(source), [source.id]);
|
|
1497
|
+
const ruleFiles = source.resolvedRules.map((rule) => {
|
|
1498
|
+
const rulePath = posix.join(adapter.managedDir, `${n}-${source.normalizedId}-${rule.resolvedPath}`);
|
|
1499
|
+
return makeAntigravityRuleFile(targetHome, rulePath, sectionForRule(source, rule), [source.id, rule.id]);
|
|
1500
|
+
});
|
|
1501
|
+
return [sourceFile, ...ruleFiles];
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
function makeAntigravityRuleFile(targetHome, relativePath, content, sourceIds) {
|
|
1505
|
+
const file = makeFile(targetHome, relativePath, "rule", content, sourceIds);
|
|
1506
|
+
if (file.content.length > ANTIGRAVITY_RULE_FILE_CHAR_LIMIT) {
|
|
1507
|
+
throw new Error(`Antigravity rule file ${file.relativePath} is ${file.content.length} characters; split it before rendering because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
|
|
1508
|
+
}
|
|
1509
|
+
return file;
|
|
1510
|
+
}
|
|
1511
|
+
function buildFiles(targetHome, adapter, profile, sources) {
|
|
1512
|
+
switch (adapter.mode) {
|
|
1513
|
+
case "native-imports":
|
|
1514
|
+
return buildNativeImportFiles(targetHome, adapter, profile, sources);
|
|
1515
|
+
case "flattened-markdown":
|
|
1516
|
+
return buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources);
|
|
1179
1517
|
case "cursor-mdc":
|
|
1180
|
-
return
|
|
1181
|
-
case "
|
|
1182
|
-
return
|
|
1518
|
+
return buildCursorRuleFiles(targetHome, adapter, sources);
|
|
1519
|
+
case "opencode-instructions":
|
|
1520
|
+
return buildOpenCodeFiles(targetHome, adapter, profile, sources);
|
|
1521
|
+
case "antigravity-rules":
|
|
1522
|
+
return buildAntigravityRuleFiles(targetHome, adapter, sources);
|
|
1183
1523
|
}
|
|
1184
1524
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1525
|
+
function adapterFor(input) {
|
|
1526
|
+
if (input.tool !== "codewith")
|
|
1527
|
+
return SESSION_TOOL_ADAPTERS[input.tool];
|
|
1528
|
+
const gatedNativeImports = input.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true";
|
|
1529
|
+
return gatedNativeImports ? CODEWITH_NATIVE_ADAPTER : CODEWITH_FLATTENED_ADAPTER;
|
|
1530
|
+
}
|
|
1531
|
+
function getHomeDir() {
|
|
1188
1532
|
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir2();
|
|
1189
1533
|
}
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1192
|
-
|
|
1534
|
+
function cleanSessionPathInput(path) {
|
|
1535
|
+
const trimmed = path.trim();
|
|
1536
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
1537
|
+
return trimmed.slice(1, -1);
|
|
1193
1538
|
}
|
|
1194
|
-
return
|
|
1539
|
+
return trimmed;
|
|
1195
1540
|
}
|
|
1196
|
-
function
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1541
|
+
function resolveSessionPath(path) {
|
|
1542
|
+
const cleaned = cleanSessionPathInput(path);
|
|
1543
|
+
if (!cleaned)
|
|
1544
|
+
throw new Error("Session render path cannot be empty.");
|
|
1545
|
+
const home = getHomeDir();
|
|
1546
|
+
if (cleaned === "~")
|
|
1547
|
+
return resolve(home);
|
|
1548
|
+
if (cleaned.startsWith("~/"))
|
|
1549
|
+
return resolve(home, cleaned.slice(2));
|
|
1550
|
+
if (cleaned === "{{HOME}}" || cleaned === "${HOME}")
|
|
1551
|
+
return resolve(home);
|
|
1552
|
+
if (cleaned.startsWith("{{HOME}}/"))
|
|
1553
|
+
return resolve(home, cleaned.slice("{{HOME}}/".length));
|
|
1554
|
+
if (cleaned.startsWith("${HOME}/"))
|
|
1555
|
+
return resolve(home, cleaned.slice("${HOME}/".length));
|
|
1556
|
+
return resolve(cleaned);
|
|
1557
|
+
}
|
|
1558
|
+
function assertSafeRelativePath(relativePath) {
|
|
1559
|
+
if (!relativePath.trim())
|
|
1560
|
+
throw new Error("Session render relative path cannot be empty.");
|
|
1561
|
+
if (relativePath.includes("\\"))
|
|
1562
|
+
throw new Error(`Session render relative path must use POSIX separators: ${relativePath}`);
|
|
1563
|
+
const normalized = posix.normalize(relativePath);
|
|
1564
|
+
if (normalized === "." || posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
|
|
1565
|
+
throw new Error(`Session render relative path escapes target root: ${relativePath}`);
|
|
1218
1566
|
}
|
|
1567
|
+
return normalized;
|
|
1219
1568
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if (
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1569
|
+
function assertSafeTargetRoot(targetHome) {
|
|
1570
|
+
if (!isAbsolute(targetHome))
|
|
1571
|
+
throw new Error(`Session render target must be an absolute path: ${targetHome}`);
|
|
1572
|
+
const normalized = resolve(targetHome);
|
|
1573
|
+
if (normalized === parse(normalized).root) {
|
|
1574
|
+
throw new Error(`Session render target cannot be the filesystem root: ${targetHome}`);
|
|
1575
|
+
}
|
|
1576
|
+
return normalized;
|
|
1577
|
+
}
|
|
1578
|
+
function resolveRenderTarget(input) {
|
|
1579
|
+
if (input.tool === "cursor" || input.tool === "antigravity") {
|
|
1580
|
+
if (!input.projectRoot) {
|
|
1581
|
+
const label = input.tool === "cursor" ? "Cursor rules" : "Antigravity rules";
|
|
1582
|
+
const path = input.tool === "cursor" ? ".cursor/rules files" : ".agents/rules files";
|
|
1583
|
+
return {
|
|
1584
|
+
targetHome: defaultTargetHome(input.tool, input.profile, input.sessionId),
|
|
1585
|
+
targetKind: "blocked",
|
|
1586
|
+
blockers: [
|
|
1587
|
+
`${label} are project-scoped; pass --project-root (or projectRoot) before applying ${path}. --target-home is not treated as a repository root for ${input.tool}.`
|
|
1588
|
+
]
|
|
1589
|
+
};
|
|
1234
1590
|
}
|
|
1235
|
-
|
|
1591
|
+
return {
|
|
1592
|
+
targetHome: resolveSessionPath(input.projectRoot),
|
|
1593
|
+
targetKind: "project-root",
|
|
1594
|
+
blockers: []
|
|
1595
|
+
};
|
|
1236
1596
|
}
|
|
1237
1597
|
return {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
new_content: renderedContent,
|
|
1242
|
-
dry_run: opts.dryRun ?? false,
|
|
1243
|
-
changed,
|
|
1244
|
-
...meta
|
|
1598
|
+
targetHome: input.targetHome ? resolveSessionPath(input.targetHome) : defaultTargetHome(input.tool, input.profile, input.sessionId),
|
|
1599
|
+
targetKind: "session-home",
|
|
1600
|
+
blockers: []
|
|
1245
1601
|
};
|
|
1246
1602
|
}
|
|
1247
|
-
function
|
|
1248
|
-
if (
|
|
1249
|
-
return
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
1258
|
-
}
|
|
1259
|
-
const store = opts.store ?? resolveConfigStore();
|
|
1260
|
-
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
1261
|
-
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
1262
|
-
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
1263
|
-
}
|
|
1264
|
-
const outputResults = [];
|
|
1265
|
-
for (const output of selectedOutputs) {
|
|
1266
|
-
outputResults.push(await applyOutput(config, output, contextConfigs, opts));
|
|
1267
|
-
}
|
|
1268
|
-
let result;
|
|
1269
|
-
if (config.target_path && shouldApplyPrimary) {
|
|
1270
|
-
result = await writeConfigResult(config, config.target_path, config.content, opts);
|
|
1271
|
-
result.outputs = outputResults;
|
|
1272
|
-
result.changed = result.changed || outputResults.some((output) => output.changed);
|
|
1273
|
-
} else {
|
|
1274
|
-
result = {
|
|
1275
|
-
...outputResults[0],
|
|
1276
|
-
outputs: outputResults.slice(1),
|
|
1277
|
-
changed: outputResults.some((output) => output.changed)
|
|
1603
|
+
function resolveSessionTargetOwnership(input, target) {
|
|
1604
|
+
if (target.targetKind === "blocked") {
|
|
1605
|
+
return {
|
|
1606
|
+
kind: "blocked",
|
|
1607
|
+
tool: input.tool,
|
|
1608
|
+
profile: input.profile,
|
|
1609
|
+
targetHome: target.targetHome,
|
|
1610
|
+
projectRoot: input.projectRoot ? resolveSessionPath(input.projectRoot) : null,
|
|
1611
|
+
ownedBy: "open-configs",
|
|
1612
|
+
reason: "target resolution blocked before provider files can be owned"
|
|
1278
1613
|
};
|
|
1279
1614
|
}
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
async function applyConfigs(configs, opts = {}) {
|
|
1293
|
-
const results = [];
|
|
1294
|
-
for (const config of configs) {
|
|
1295
|
-
if (config.kind === "reference")
|
|
1296
|
-
continue;
|
|
1297
|
-
results.push(await applyConfig(config, opts));
|
|
1298
|
-
}
|
|
1299
|
-
return results;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// src/lib/redact.ts
|
|
1303
|
-
var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
|
|
1304
|
-
var VALUE_PATTERNS = [
|
|
1305
|
-
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
1306
|
-
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
1307
|
-
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
1308
|
-
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
1309
|
-
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
1310
|
-
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
1311
|
-
{ re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
|
|
1312
|
-
{ re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
|
|
1313
|
-
];
|
|
1314
|
-
var MIN_SECRET_VALUE_LEN = 8;
|
|
1315
|
-
function redactShell(content) {
|
|
1316
|
-
const redacted = [];
|
|
1317
|
-
const lines = content.split(`
|
|
1318
|
-
`);
|
|
1319
|
-
const out = [];
|
|
1320
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1321
|
-
const line = lines[i];
|
|
1322
|
-
const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
1323
|
-
if (m) {
|
|
1324
|
-
const [, prefix, key, eq, quote, value] = m;
|
|
1325
|
-
if (shouldRedactKeyValue(key, value)) {
|
|
1326
|
-
const reason = reasonFor(key, value);
|
|
1327
|
-
redacted.push({ varName: key, line: i + 1, reason });
|
|
1328
|
-
out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
out.push(line);
|
|
1615
|
+
if (target.targetKind === "project-root") {
|
|
1616
|
+
return {
|
|
1617
|
+
kind: "project",
|
|
1618
|
+
tool: input.tool,
|
|
1619
|
+
profile: input.profile,
|
|
1620
|
+
targetHome: target.targetHome,
|
|
1621
|
+
projectRoot: target.targetHome,
|
|
1622
|
+
ownedBy: "open-configs",
|
|
1623
|
+
reason: "project-scoped provider files are generated in the explicit repository root"
|
|
1624
|
+
};
|
|
1333
1625
|
}
|
|
1334
|
-
return {
|
|
1335
|
-
|
|
1626
|
+
return {
|
|
1627
|
+
kind: "provider-profile",
|
|
1628
|
+
tool: input.tool,
|
|
1629
|
+
profile: input.profile,
|
|
1630
|
+
targetHome: target.targetHome,
|
|
1631
|
+
projectRoot: null,
|
|
1632
|
+
ownedBy: "open-configs",
|
|
1633
|
+
reason: "profile-scoped provider home is generated by OpenConfigs from identity/config sources"
|
|
1634
|
+
};
|
|
1336
1635
|
}
|
|
1337
|
-
function
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
let newLine = line;
|
|
1355
|
-
for (const { re, reason } of VALUE_PATTERNS) {
|
|
1356
|
-
newLine = newLine.replace(re, (match) => {
|
|
1357
|
-
const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
1358
|
-
redacted.push({ varName, line: i + 1, reason });
|
|
1359
|
-
return `{{${varName}}}`;
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
out.push(newLine);
|
|
1636
|
+
function planSessionRender(input) {
|
|
1637
|
+
if (!SESSION_RENDER_TOOLS.includes(input.tool))
|
|
1638
|
+
throw new Error(`Unsupported session render tool: ${input.tool}`);
|
|
1639
|
+
if (!input.profile.trim())
|
|
1640
|
+
throw new Error("Session render profile is required.");
|
|
1641
|
+
const adapter = adapterFor(input);
|
|
1642
|
+
const { targetHome, targetKind, blockers } = resolveRenderTarget(input);
|
|
1643
|
+
const targetOwner = resolveSessionTargetOwnership(input, { targetHome, targetKind });
|
|
1644
|
+
const blocked = blockers.length > 0;
|
|
1645
|
+
const allowEmptySources = input.allowEmptySources === true;
|
|
1646
|
+
const orderedSources = composeSources(normalizeSources(input.sources, input.tool, allowEmptySources));
|
|
1647
|
+
if (orderedSources.length === 0 && !allowEmptySources) {
|
|
1648
|
+
throw new Error("Session render has no instruction sources. Pass --allow-empty-sources only for explicit empty renders.");
|
|
1363
1649
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1650
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
1651
|
+
const env = adapter.envVar && !blocked ? { [adapter.envVar]: targetHome } : {};
|
|
1652
|
+
const warnings = [
|
|
1653
|
+
...orderedSources.length === 0 ? ["No instruction sources were provided."] : [],
|
|
1654
|
+
...blockers
|
|
1655
|
+
];
|
|
1656
|
+
const files = blocked ? [] : buildFiles(targetHome, adapter, input.profile, orderedSources);
|
|
1657
|
+
rejectDuplicateRenderPaths(files);
|
|
1658
|
+
const manifest = {
|
|
1659
|
+
schema: SESSION_RENDER_SCHEMA,
|
|
1660
|
+
tool: input.tool,
|
|
1661
|
+
adapterMode: adapter.mode,
|
|
1662
|
+
profile: input.profile,
|
|
1663
|
+
sessionId: input.sessionId ?? null,
|
|
1664
|
+
targetHome,
|
|
1665
|
+
targetKind,
|
|
1666
|
+
targetOwner,
|
|
1667
|
+
writable: !blocked,
|
|
1668
|
+
blocked,
|
|
1669
|
+
blockers,
|
|
1670
|
+
generatedAt,
|
|
1671
|
+
env,
|
|
1672
|
+
sourceHash: fingerprint(orderedSources.map((source) => ({
|
|
1673
|
+
id: source.id,
|
|
1674
|
+
layer: source.resolvedLayer,
|
|
1675
|
+
order: source.resolvedOrder,
|
|
1676
|
+
merge: source.resolvedMerge,
|
|
1677
|
+
content: source.content,
|
|
1678
|
+
rules: source.resolvedRules.map((rule) => ({ id: rule.id, path: rule.resolvedPath, content: rule.content })),
|
|
1679
|
+
hash: source.hash ?? null
|
|
1680
|
+
}))),
|
|
1681
|
+
sources: orderedSources.map((source) => ({
|
|
1682
|
+
id: source.id,
|
|
1683
|
+
label: source.resolvedLabel,
|
|
1684
|
+
layer: source.resolvedLayer,
|
|
1685
|
+
merge: source.resolvedMerge,
|
|
1686
|
+
order: source.resolvedOrder,
|
|
1687
|
+
path: source.path ?? null,
|
|
1688
|
+
targetProviders: source.targetProviders ?? [],
|
|
1689
|
+
owner: source.owner ?? null,
|
|
1690
|
+
sourcePaths: source.sourcePaths ?? [],
|
|
1691
|
+
hash: source.hash ?? null,
|
|
1692
|
+
nonOverridable: source.nonOverridable === true,
|
|
1693
|
+
replacementScope: source.replacementScope ?? null,
|
|
1694
|
+
rules: source.resolvedRules.map((rule) => ({
|
|
1695
|
+
id: rule.id,
|
|
1696
|
+
label: rule.resolvedLabel,
|
|
1697
|
+
path: rule.resolvedPath,
|
|
1698
|
+
globs: rule.globs ?? [],
|
|
1699
|
+
hash: rule.hash ?? null
|
|
1700
|
+
})),
|
|
1701
|
+
provenance: source.provenance ?? null
|
|
1702
|
+
})),
|
|
1703
|
+
skippedSources: [],
|
|
1704
|
+
files: files.map((file) => ({
|
|
1705
|
+
path: file.path,
|
|
1706
|
+
relativePath: file.relativePath,
|
|
1707
|
+
role: file.role,
|
|
1708
|
+
sha256: file.sha256,
|
|
1709
|
+
sourceIds: file.sourceIds
|
|
1710
|
+
})),
|
|
1711
|
+
warnings
|
|
1712
|
+
};
|
|
1713
|
+
const manifestFile = makeFile(targetHome, posix.join(".hasna", "session-render-manifest.json"), "manifest", JSON.stringify(manifest, null, 2), orderedSources.map((source) => source.id));
|
|
1714
|
+
return {
|
|
1715
|
+
dryRun: true,
|
|
1716
|
+
tool: input.tool,
|
|
1717
|
+
adapter,
|
|
1718
|
+
profile: input.profile,
|
|
1719
|
+
sessionId: input.sessionId ?? null,
|
|
1720
|
+
targetHome,
|
|
1721
|
+
targetKind,
|
|
1722
|
+
targetOwner,
|
|
1723
|
+
writable: !blocked,
|
|
1724
|
+
blocked,
|
|
1725
|
+
blockers,
|
|
1726
|
+
env,
|
|
1727
|
+
files,
|
|
1728
|
+
manifest,
|
|
1729
|
+
manifestFile,
|
|
1730
|
+
allFiles: [...files, manifestFile],
|
|
1731
|
+
warnings
|
|
1732
|
+
};
|
|
1366
1733
|
}
|
|
1367
|
-
function
|
|
1368
|
-
const
|
|
1369
|
-
const lines = content.split(`
|
|
1370
|
-
`);
|
|
1371
|
-
const out = [];
|
|
1372
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1373
|
-
const line = lines[i];
|
|
1374
|
-
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
1375
|
-
if (m) {
|
|
1376
|
-
const [, indent, key, eq, quote, value] = m;
|
|
1377
|
-
if (shouldRedactKeyValue(key, value)) {
|
|
1378
|
-
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1379
|
-
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
1380
|
-
out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
out.push(line);
|
|
1385
|
-
}
|
|
1386
|
-
return { content: out.join(`
|
|
1387
|
-
`), redacted, isTemplate: redacted.length > 0 };
|
|
1388
|
-
}
|
|
1389
|
-
function redactIni(content) {
|
|
1390
|
-
const redacted = [];
|
|
1391
|
-
const lines = content.split(`
|
|
1392
|
-
`);
|
|
1393
|
-
const out = [];
|
|
1394
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1395
|
-
const line = lines[i];
|
|
1396
|
-
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
1397
|
-
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
1398
|
-
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
1399
|
-
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
1403
|
-
if (m) {
|
|
1404
|
-
const [, indent, key, eq, value] = m;
|
|
1405
|
-
if (shouldRedactKeyValue(key, value)) {
|
|
1406
|
-
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1407
|
-
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
1408
|
-
out.push(`${indent}${key}${eq}{{${varName}}}`);
|
|
1409
|
-
continue;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
out.push(line);
|
|
1413
|
-
}
|
|
1414
|
-
return { content: out.join(`
|
|
1415
|
-
`), redacted, isTemplate: redacted.length > 0 };
|
|
1416
|
-
}
|
|
1417
|
-
function redactGeneric(content) {
|
|
1418
|
-
const redacted = [];
|
|
1419
|
-
const lines = content.split(`
|
|
1420
|
-
`);
|
|
1421
|
-
const out = [];
|
|
1422
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1423
|
-
let line = lines[i];
|
|
1424
|
-
for (const { re, reason } of VALUE_PATTERNS) {
|
|
1425
|
-
line = line.replace(re, (match) => {
|
|
1426
|
-
const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1427
|
-
redacted.push({ varName, line: i + 1, reason });
|
|
1428
|
-
return `{{${varName}}}`;
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
out.push(line);
|
|
1432
|
-
}
|
|
1433
|
-
return { content: out.join(`
|
|
1434
|
-
`), redacted, isTemplate: redacted.length > 0 };
|
|
1435
|
-
}
|
|
1436
|
-
function shouldRedactKeyValue(key, value) {
|
|
1437
|
-
if (!value || value.startsWith("{{"))
|
|
1438
|
-
return false;
|
|
1439
|
-
if (isReferenceValue(value.trim()))
|
|
1440
|
-
return false;
|
|
1441
|
-
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
1442
|
-
return false;
|
|
1443
|
-
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
1444
|
-
return false;
|
|
1445
|
-
if (SECRET_KEY_PATTERN.test(key))
|
|
1446
|
-
return true;
|
|
1447
|
-
for (const { re } of VALUE_PATTERNS) {
|
|
1448
|
-
if (re.test(value))
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
return false;
|
|
1452
|
-
}
|
|
1453
|
-
function reasonFor(key, value) {
|
|
1454
|
-
if (SECRET_KEY_PATTERN.test(key))
|
|
1455
|
-
return `secret key name: ${key}`;
|
|
1456
|
-
for (const { re, reason } of VALUE_PATTERNS) {
|
|
1457
|
-
if (re.test(value))
|
|
1458
|
-
return reason;
|
|
1459
|
-
}
|
|
1460
|
-
return "secret value pattern";
|
|
1461
|
-
}
|
|
1462
|
-
function isReferenceValue(value) {
|
|
1463
|
-
return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
|
|
1464
|
-
}
|
|
1465
|
-
function redactContent(content, format) {
|
|
1466
|
-
switch (format) {
|
|
1467
|
-
case "shell":
|
|
1468
|
-
return redactShell(content);
|
|
1469
|
-
case "json":
|
|
1470
|
-
return redactJson(content);
|
|
1471
|
-
case "toml":
|
|
1472
|
-
return redactToml(content);
|
|
1473
|
-
case "ini":
|
|
1474
|
-
return redactIni(content);
|
|
1475
|
-
default:
|
|
1476
|
-
return redactGeneric(content);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function scanSecrets(content, format) {
|
|
1480
|
-
const r = redactContent(content, format);
|
|
1481
|
-
return r.redacted;
|
|
1482
|
-
}
|
|
1483
|
-
function hasSecrets(content, format) {
|
|
1484
|
-
return scanSecrets(content, format).length > 0;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// src/status.ts
|
|
1488
|
-
var PACKAGE_NAME = "@hasna/instructions";
|
|
1489
|
-
var PACKAGE_VERSION = "0.3.0";
|
|
1490
|
-
function activeDatabaseEnv() {
|
|
1491
|
-
if (process.env["HASNA_INSTRUCTIONS_DB_PATH"])
|
|
1492
|
-
return "HASNA_INSTRUCTIONS_DB_PATH";
|
|
1493
|
-
return null;
|
|
1494
|
-
}
|
|
1495
|
-
function configuredDatabaseKind() {
|
|
1496
|
-
const value = process.env["HASNA_INSTRUCTIONS_DB_PATH"] ?? "";
|
|
1497
|
-
return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
|
|
1498
|
-
}
|
|
1499
|
-
function countBy(items, getValue) {
|
|
1500
|
-
const counts = {};
|
|
1501
|
-
for (const item of items) {
|
|
1502
|
-
const value = getValue(item);
|
|
1503
|
-
if (!value)
|
|
1504
|
-
continue;
|
|
1505
|
-
counts[value] = (counts[value] ?? 0) + 1;
|
|
1506
|
-
}
|
|
1507
|
-
return counts;
|
|
1508
|
-
}
|
|
1509
|
-
async function getConfigsStatus(store = resolveConfigStore()) {
|
|
1510
|
-
let databaseReachable = true;
|
|
1511
|
-
let configs = [];
|
|
1512
|
-
let categoryStats = { total: 0 };
|
|
1513
|
-
try {
|
|
1514
|
-
configs = await store.listConfigs();
|
|
1515
|
-
categoryStats = await store.getConfigStats();
|
|
1516
|
-
} catch {
|
|
1517
|
-
databaseReachable = false;
|
|
1518
|
-
}
|
|
1519
|
-
const fileConfigs = configs.filter((config) => config.kind === "file");
|
|
1520
|
-
let driftedTargets = 0;
|
|
1521
|
-
let missingTargets = 0;
|
|
1522
|
-
let unredactedSecretFindings = 0;
|
|
1523
|
-
let knownTargets = 0;
|
|
1524
|
-
for (const config of fileConfigs) {
|
|
1525
|
-
unredactedSecretFindings += scanSecrets(config.content, config.format).length;
|
|
1526
|
-
if (!config.target_path)
|
|
1527
|
-
continue;
|
|
1528
|
-
knownTargets += 1;
|
|
1529
|
-
const targetPath = expandPath(config.target_path);
|
|
1530
|
-
if (!existsSync4(targetPath)) {
|
|
1531
|
-
missingTargets += 1;
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
const disk = readFileSync2(targetPath, "utf-8");
|
|
1535
|
-
const { content: redactedDisk } = redactContent(disk, config.format);
|
|
1536
|
-
if (redactedDisk !== config.content) {
|
|
1537
|
-
driftedTargets += 1;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
let profiles = 0;
|
|
1541
|
-
let machines = 0;
|
|
1542
|
-
let profileLinks = 0;
|
|
1543
|
-
let snapshots = 0;
|
|
1544
|
-
if (databaseReachable) {
|
|
1545
|
-
try {
|
|
1546
|
-
const profileList = await store.listProfiles();
|
|
1547
|
-
profiles = profileList.length;
|
|
1548
|
-
machines = (await store.listMachines()).length;
|
|
1549
|
-
for (const profile of profileList) {
|
|
1550
|
-
profileLinks += (await store.getProfileConfigs(profile.id)).length;
|
|
1551
|
-
}
|
|
1552
|
-
for (const config of configs) {
|
|
1553
|
-
snapshots += (await store.listSnapshots(config.id)).length;
|
|
1554
|
-
}
|
|
1555
|
-
} catch {
|
|
1556
|
-
databaseReachable = false;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
|
|
1560
|
-
const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
|
|
1561
|
-
return {
|
|
1562
|
-
service: "configs",
|
|
1563
|
-
schemaVersion: "1.0",
|
|
1564
|
-
package: {
|
|
1565
|
-
name: PACKAGE_NAME,
|
|
1566
|
-
version: PACKAGE_VERSION
|
|
1567
|
-
},
|
|
1568
|
-
env: {
|
|
1569
|
-
database: {
|
|
1570
|
-
primary: "HASNA_INSTRUCTIONS_DB_PATH",
|
|
1571
|
-
active: activeDatabaseEnv(),
|
|
1572
|
-
kind: configuredDatabaseKind()
|
|
1573
|
-
}
|
|
1574
|
-
},
|
|
1575
|
-
counts: {
|
|
1576
|
-
configs: {
|
|
1577
|
-
total: configs.length,
|
|
1578
|
-
file: fileConfigs.length,
|
|
1579
|
-
reference: configs.filter((config) => config.kind === "reference").length,
|
|
1580
|
-
templates: configs.filter((config) => config.is_template).length
|
|
1581
|
-
},
|
|
1582
|
-
byCategory,
|
|
1583
|
-
byAgent: countBy(configs, (config) => config.agent),
|
|
1584
|
-
byFormat: countBy(configs, (config) => config.format),
|
|
1585
|
-
profiles,
|
|
1586
|
-
profileLinks,
|
|
1587
|
-
machines,
|
|
1588
|
-
snapshots,
|
|
1589
|
-
knownTargets
|
|
1590
|
-
},
|
|
1591
|
-
health: {
|
|
1592
|
-
status,
|
|
1593
|
-
databaseReachable,
|
|
1594
|
-
driftedTargets,
|
|
1595
|
-
missingTargets,
|
|
1596
|
-
unredactedSecretFindings,
|
|
1597
|
-
hasDrift: driftedTargets > 0,
|
|
1598
|
-
hasMissingTargets: missingTargets > 0,
|
|
1599
|
-
hasUnredactedSecrets: unredactedSecretFindings > 0
|
|
1600
|
-
},
|
|
1601
|
-
safety: {
|
|
1602
|
-
includesConfigValues: false,
|
|
1603
|
-
includesPrivatePaths: false,
|
|
1604
|
-
includesHostnames: false,
|
|
1605
|
-
includesSecretValues: false,
|
|
1606
|
-
statusOutputIsMetadataOnly: true
|
|
1607
|
-
}
|
|
1608
|
-
};
|
|
1609
|
-
}
|
|
1610
|
-
// src/db/pg-migrations.ts
|
|
1611
|
-
var PG_MIGRATIONS = [
|
|
1612
|
-
`CREATE TABLE IF NOT EXISTS configs (
|
|
1613
|
-
id TEXT PRIMARY KEY,
|
|
1614
|
-
name TEXT NOT NULL,
|
|
1615
|
-
slug TEXT NOT NULL UNIQUE,
|
|
1616
|
-
kind TEXT NOT NULL DEFAULT 'file',
|
|
1617
|
-
category TEXT NOT NULL,
|
|
1618
|
-
agent TEXT NOT NULL DEFAULT 'global',
|
|
1619
|
-
target_path TEXT,
|
|
1620
|
-
outputs TEXT NOT NULL DEFAULT '[]',
|
|
1621
|
-
format TEXT NOT NULL DEFAULT 'text',
|
|
1622
|
-
content TEXT NOT NULL DEFAULT '',
|
|
1623
|
-
description TEXT,
|
|
1624
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
1625
|
-
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
1626
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1627
|
-
created_at TEXT NOT NULL,
|
|
1628
|
-
updated_at TEXT NOT NULL,
|
|
1629
|
-
synced_at TEXT
|
|
1630
|
-
)`,
|
|
1631
|
-
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
1632
|
-
id TEXT PRIMARY KEY,
|
|
1633
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1634
|
-
content TEXT NOT NULL,
|
|
1635
|
-
version INTEGER NOT NULL,
|
|
1636
|
-
created_at TEXT NOT NULL
|
|
1637
|
-
)`,
|
|
1638
|
-
`CREATE TABLE IF NOT EXISTS profiles (
|
|
1639
|
-
id TEXT PRIMARY KEY,
|
|
1640
|
-
name TEXT NOT NULL,
|
|
1641
|
-
slug TEXT NOT NULL UNIQUE,
|
|
1642
|
-
description TEXT,
|
|
1643
|
-
selectors TEXT NOT NULL DEFAULT '{}',
|
|
1644
|
-
variables TEXT NOT NULL DEFAULT '{}',
|
|
1645
|
-
created_at TEXT NOT NULL,
|
|
1646
|
-
updated_at TEXT NOT NULL
|
|
1647
|
-
)`,
|
|
1648
|
-
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
1649
|
-
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
1650
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1651
|
-
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1652
|
-
PRIMARY KEY (profile_id, config_id)
|
|
1653
|
-
)`,
|
|
1654
|
-
`CREATE TABLE IF NOT EXISTS machines (
|
|
1655
|
-
id TEXT PRIMARY KEY,
|
|
1656
|
-
hostname TEXT NOT NULL UNIQUE,
|
|
1657
|
-
os TEXT,
|
|
1658
|
-
arch TEXT,
|
|
1659
|
-
last_applied_at TEXT,
|
|
1660
|
-
created_at TEXT NOT NULL
|
|
1661
|
-
)`,
|
|
1662
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1663
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1664
|
-
message TEXT NOT NULL,
|
|
1665
|
-
email TEXT,
|
|
1666
|
-
category TEXT DEFAULT 'general',
|
|
1667
|
-
version TEXT,
|
|
1668
|
-
machine_id TEXT,
|
|
1669
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1670
|
-
)`,
|
|
1671
|
-
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
1672
|
-
];
|
|
1673
|
-
// src/lib/session-render.ts
|
|
1674
|
-
import { createHash } from "crypto";
|
|
1675
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync } from "fs";
|
|
1676
|
-
import { homedir as homedir3 } from "os";
|
|
1677
|
-
import { basename as basename3, dirname as dirname2, extname as extname2, isAbsolute, join as join3, parse, posix, relative, resolve as resolve2 } from "path";
|
|
1678
|
-
var CODEWITH_NATIVE_IMPORTS_ENV = "HASNA_CONFIGS_CODEWITH_NATIVE_IMPORTS";
|
|
1679
|
-
var SESSION_RENDER_MANAGED_MARKER = "Managed by @hasna/configs session render";
|
|
1680
|
-
var SESSION_RENDER_SCHEMA = "hasna.configs.session-render/v1";
|
|
1681
|
-
var RAW_STORE_ROOT_ENV = "HASNA_CONFIGS_HOME";
|
|
1682
|
-
var SESSION_RENDER_TOOLS = [
|
|
1683
|
-
"claude",
|
|
1684
|
-
"codex",
|
|
1685
|
-
"cursor",
|
|
1686
|
-
"opencode",
|
|
1687
|
-
"codewith"
|
|
1688
|
-
];
|
|
1689
|
-
var CODEWITH_FLATTENED_ADAPTER = {
|
|
1690
|
-
tool: "codewith",
|
|
1691
|
-
mode: "flattened-markdown",
|
|
1692
|
-
indexFile: "CODEWITH.md",
|
|
1693
|
-
managedDir: ".hasna/instructions",
|
|
1694
|
-
envVar: "CODEWITH_HOME",
|
|
1695
|
-
nativeImports: false,
|
|
1696
|
-
description: "Codewith CODEWITH.md flattened until native @ imports are implemented in Codewith."
|
|
1697
|
-
};
|
|
1698
|
-
var CODEWITH_NATIVE_ADAPTER = {
|
|
1699
|
-
tool: "codewith",
|
|
1700
|
-
mode: "native-imports",
|
|
1701
|
-
indexFile: "CODEWITH.md",
|
|
1702
|
-
managedDir: ".hasna/instructions",
|
|
1703
|
-
envVar: "CODEWITH_HOME",
|
|
1704
|
-
nativeImports: true,
|
|
1705
|
-
description: "Codewith CODEWITH.md with gated @ imports into managed fragments."
|
|
1706
|
-
};
|
|
1707
|
-
var SESSION_TOOL_ADAPTERS = {
|
|
1708
|
-
claude: {
|
|
1709
|
-
tool: "claude",
|
|
1710
|
-
mode: "native-imports",
|
|
1711
|
-
indexFile: "CLAUDE.md",
|
|
1712
|
-
managedDir: ".hasna/instructions",
|
|
1713
|
-
envVar: "CLAUDE_CONFIG_DIR",
|
|
1714
|
-
nativeImports: true,
|
|
1715
|
-
description: "Claude Code CLAUDE.md with @ imports into managed fragments."
|
|
1716
|
-
},
|
|
1717
|
-
codex: {
|
|
1718
|
-
tool: "codex",
|
|
1719
|
-
mode: "flattened-markdown",
|
|
1720
|
-
indexFile: "AGENTS.md",
|
|
1721
|
-
managedDir: ".hasna/instructions",
|
|
1722
|
-
envVar: "CODEX_HOME",
|
|
1723
|
-
nativeImports: false,
|
|
1724
|
-
description: "Codex AGENTS.md flattened instruction file."
|
|
1725
|
-
},
|
|
1726
|
-
cursor: {
|
|
1727
|
-
tool: "cursor",
|
|
1728
|
-
mode: "cursor-mdc",
|
|
1729
|
-
managedDir: ".cursor/rules",
|
|
1730
|
-
nativeImports: false,
|
|
1731
|
-
description: "Cursor project rule files in .cursor/rules/*.mdc."
|
|
1732
|
-
},
|
|
1733
|
-
opencode: {
|
|
1734
|
-
tool: "opencode",
|
|
1735
|
-
mode: "opencode-instructions",
|
|
1736
|
-
indexFile: "AGENTS.md",
|
|
1737
|
-
configFile: "opencode.json",
|
|
1738
|
-
managedDir: ".hasna/instructions",
|
|
1739
|
-
envVar: "OPENCODE_CONFIG_DIR",
|
|
1740
|
-
nativeImports: false,
|
|
1741
|
-
description: "OpenCode AGENTS.md plus opencode.json instructions pointing at managed fragments."
|
|
1742
|
-
},
|
|
1743
|
-
codewith: CODEWITH_FLATTENED_ADAPTER
|
|
1744
|
-
};
|
|
1745
|
-
var LAYER_RANK = {
|
|
1746
|
-
global: 10,
|
|
1747
|
-
tool: 20,
|
|
1748
|
-
account: 30,
|
|
1749
|
-
agent: 40,
|
|
1750
|
-
project: 50,
|
|
1751
|
-
local: 60
|
|
1752
|
-
};
|
|
1753
|
-
function ensureTrailingNewline2(content) {
|
|
1754
|
-
return content.endsWith(`
|
|
1755
|
-
`) ? content : `${content}
|
|
1756
|
-
`;
|
|
1757
|
-
}
|
|
1758
|
-
function sha256(content) {
|
|
1759
|
-
return createHash("sha256").update(content).digest("hex");
|
|
1760
|
-
}
|
|
1761
|
-
function fingerprint(value) {
|
|
1762
|
-
return sha256(JSON.stringify(value));
|
|
1763
|
-
}
|
|
1764
|
-
function slug(value) {
|
|
1765
|
-
const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1766
|
-
return s || "instruction";
|
|
1767
|
-
}
|
|
1768
|
-
function yamlQuote2(value) {
|
|
1769
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
1770
|
-
}
|
|
1771
|
-
function getRawStoreRoot() {
|
|
1772
|
-
return resolve2(process.env[RAW_STORE_ROOT_ENV] || join3(process.env["HOME"] || homedir3(), ".hasna", "configs"));
|
|
1773
|
-
}
|
|
1774
|
-
function defaultTargetHome(tool, profile, sessionId) {
|
|
1775
|
-
return join3(getRawStoreRoot(), "sessions", tool, slug(profile), slug(sessionId || "latest"));
|
|
1776
|
-
}
|
|
1777
|
-
function joinTarget(targetHome, relativePath) {
|
|
1778
|
-
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
1779
|
-
const safeRelativePath = assertSafeRelativePath(relativePath);
|
|
1780
|
-
return join3(safeTargetHome, ...safeRelativePath.split("/"));
|
|
1781
|
-
}
|
|
1782
|
-
function makeFile(targetHome, relativePath, role, content, sourceIds) {
|
|
1783
|
-
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
1784
|
-
const safeRelativePath = assertSafeRelativePath(relativePath);
|
|
1785
|
-
const normalizedContent = ensureTrailingNewline2(content);
|
|
1734
|
+
function sourceFromFilePath(path, content, order = 0) {
|
|
1735
|
+
const file = basename(path);
|
|
1786
1736
|
return {
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
function normalizeSources(sources, tool, allowEmptySources) {
|
|
1796
|
-
const ordered = sources.map((source, index) => {
|
|
1797
|
-
if (!source.id.trim())
|
|
1798
|
-
throw new Error("Session instruction source id is required.");
|
|
1799
|
-
const content = filterProviderOnlyBlocks(source.content ?? "", tool);
|
|
1800
|
-
const normalized = {
|
|
1801
|
-
...source,
|
|
1802
|
-
content,
|
|
1803
|
-
normalizedId: slug(source.id),
|
|
1804
|
-
resolvedLabel: source.label ?? source.id,
|
|
1805
|
-
resolvedLayer: source.layer ?? "agent",
|
|
1806
|
-
resolvedMerge: source.merge ?? "append",
|
|
1807
|
-
resolvedOrder: source.order ?? index,
|
|
1808
|
-
resolvedRules: normalizeInstructionRules(source, tool)
|
|
1809
|
-
};
|
|
1810
|
-
const hasPathReferences = (normalized.sourcePaths ?? []).length > 0;
|
|
1811
|
-
if (!allowEmptySources && !normalized.content.trim() && normalized.resolvedRules.length === 0 && !hasPathReferences) {
|
|
1812
|
-
throw new Error(`Session instruction source "${source.id}" is empty. Pass --allow-empty-sources only for explicit empty renders.`);
|
|
1813
|
-
}
|
|
1814
|
-
return normalized;
|
|
1815
|
-
}).sort((a, b) => LAYER_RANK[a.resolvedLayer] - LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id));
|
|
1816
|
-
rejectDuplicateSourceSlugs(ordered);
|
|
1817
|
-
rejectDuplicateRulePaths(ordered);
|
|
1818
|
-
return ordered;
|
|
1819
|
-
}
|
|
1820
|
-
function filterProviderOnlyBlocks(content, tool) {
|
|
1821
|
-
const lines = content.split(/\r?\n/);
|
|
1822
|
-
const output = [];
|
|
1823
|
-
let activeProviders = null;
|
|
1824
|
-
for (const line of lines) {
|
|
1825
|
-
const start = line.match(/^\s*<!--\s*@hasna-provider:\s*([^>]+?)\s*-->\s*$/i);
|
|
1826
|
-
if (start) {
|
|
1827
|
-
if (activeProviders)
|
|
1828
|
-
throw new Error("Nested provider-only instruction blocks are not supported.");
|
|
1829
|
-
activeProviders = start[1].split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
1830
|
-
continue;
|
|
1831
|
-
}
|
|
1832
|
-
if (/^\s*<!--\s*@hasna-end-provider\s*-->\s*$/i.test(line)) {
|
|
1833
|
-
if (!activeProviders)
|
|
1834
|
-
throw new Error("Provider-only instruction block end marker without start marker.");
|
|
1835
|
-
activeProviders = null;
|
|
1836
|
-
continue;
|
|
1837
|
-
}
|
|
1838
|
-
if (!activeProviders || activeProviders.includes(tool) || activeProviders.includes("all") || activeProviders.includes("generic")) {
|
|
1839
|
-
output.push(line);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
if (activeProviders)
|
|
1843
|
-
throw new Error("Provider-only instruction block was not closed.");
|
|
1844
|
-
return output.join(`
|
|
1845
|
-
`);
|
|
1846
|
-
}
|
|
1847
|
-
function composeSources(sources) {
|
|
1848
|
-
let start = -1;
|
|
1849
|
-
for (let i = 0;i < sources.length; i++) {
|
|
1850
|
-
if (sources[i].resolvedMerge === "replace")
|
|
1851
|
-
start = i;
|
|
1852
|
-
}
|
|
1853
|
-
if (start < 0)
|
|
1854
|
-
return sources;
|
|
1855
|
-
const protectedSources = sources.slice(0, start).filter((source) => source.nonOverridable);
|
|
1856
|
-
return [...protectedSources, ...sources.slice(start)];
|
|
1857
|
-
}
|
|
1858
|
-
function sectionForSource(source) {
|
|
1859
|
-
const parts = [
|
|
1860
|
-
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1861
|
-
`# ${source.resolvedLabel}`
|
|
1862
|
-
];
|
|
1863
|
-
if (source.path)
|
|
1864
|
-
parts.push(`Source: ${source.path}`);
|
|
1865
|
-
if (source.sourcePaths && source.sourcePaths.length > 0) {
|
|
1866
|
-
parts.push([
|
|
1867
|
-
"Source paths:",
|
|
1868
|
-
...source.sourcePaths.map((sourcePath) => {
|
|
1869
|
-
const flags = [
|
|
1870
|
-
sourcePath.editable ? "editable" : null,
|
|
1871
|
-
sourcePath.required ? "required" : null,
|
|
1872
|
-
sourcePath.hash ? sourcePath.hash : null
|
|
1873
|
-
].filter(Boolean);
|
|
1874
|
-
return `- ${sourcePath.path}${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
|
|
1875
|
-
})
|
|
1876
|
-
].join(`
|
|
1877
|
-
`));
|
|
1878
|
-
}
|
|
1879
|
-
if (source.owner)
|
|
1880
|
-
parts.push(`Owner: ${source.owner.kind}:${source.owner.id}`);
|
|
1881
|
-
const content = source.content.trim();
|
|
1882
|
-
if (content)
|
|
1883
|
-
parts.push(content);
|
|
1884
|
-
return parts.join(`
|
|
1885
|
-
|
|
1886
|
-
`);
|
|
1887
|
-
}
|
|
1888
|
-
function sectionForRule(source, rule) {
|
|
1889
|
-
const parts = [
|
|
1890
|
-
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1891
|
-
`# ${rule.resolvedLabel}`
|
|
1892
|
-
];
|
|
1893
|
-
if (source.path)
|
|
1894
|
-
parts.push(`Source: ${source.path}`);
|
|
1895
|
-
if (rule.path)
|
|
1896
|
-
parts.push(`Rule path: ${rule.path}`);
|
|
1897
|
-
const content = rule.content.trim();
|
|
1898
|
-
if (content)
|
|
1899
|
-
parts.push(content);
|
|
1900
|
-
return parts.join(`
|
|
1901
|
-
|
|
1902
|
-
`);
|
|
1903
|
-
}
|
|
1904
|
-
function fragmentPath(adapter, index, source) {
|
|
1905
|
-
const n = String(index + 1).padStart(2, "0");
|
|
1906
|
-
return posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
|
|
1907
|
-
}
|
|
1908
|
-
function ruleFragmentPath(adapter, source, rule) {
|
|
1909
|
-
return posix.join(adapter.managedDir, "rules", source.normalizedId, rule.resolvedPath);
|
|
1910
|
-
}
|
|
1911
|
-
function importPath(indexRelativePath, fragmentRelativePath) {
|
|
1912
|
-
const relative2 = posix.relative(posix.dirname(indexRelativePath), fragmentRelativePath);
|
|
1913
|
-
if (relative2.startsWith("./") || relative2.startsWith("../"))
|
|
1914
|
-
return relative2;
|
|
1915
|
-
return `./${relative2}`;
|
|
1916
|
-
}
|
|
1917
|
-
function indexHeader(tool, profile) {
|
|
1918
|
-
return [
|
|
1919
|
-
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1920
|
-
`# ${tool} session instructions`,
|
|
1921
|
-
"",
|
|
1922
|
-
`Profile: ${profile}`
|
|
1923
|
-
].join(`
|
|
1924
|
-
`);
|
|
1925
|
-
}
|
|
1926
|
-
function buildNativeImportFiles(targetHome, adapter, profile, sources) {
|
|
1927
|
-
const indexFile = adapter.indexFile;
|
|
1928
|
-
const fragments = sources.flatMap((source, index2) => [
|
|
1929
|
-
makeFile(targetHome, fragmentPath(adapter, index2, source), "fragment", sectionForSource(source), [source.id]),
|
|
1930
|
-
...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
|
|
1931
|
-
]);
|
|
1932
|
-
const imports = fragments.map((file) => `@${importPath(indexFile, file.relativePath)}`);
|
|
1933
|
-
const index = makeFile(targetHome, indexFile, "index", [indexHeader(adapter.tool, profile), ...imports].join(`
|
|
1934
|
-
`), sources.map((source) => source.id));
|
|
1935
|
-
return [index, ...fragments];
|
|
1936
|
-
}
|
|
1937
|
-
function buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources) {
|
|
1938
|
-
const content = [
|
|
1939
|
-
indexHeader(adapter.tool, profile),
|
|
1940
|
-
...sources.flatMap((source) => [
|
|
1941
|
-
sectionForSource(source),
|
|
1942
|
-
...source.resolvedRules.map((rule) => sectionForRule(source, rule))
|
|
1943
|
-
])
|
|
1944
|
-
].join(`
|
|
1945
|
-
|
|
1946
|
-
`);
|
|
1947
|
-
return [
|
|
1948
|
-
makeFile(targetHome, adapter.indexFile, "index", content, [
|
|
1949
|
-
...sources.map((source) => source.id),
|
|
1950
|
-
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
1951
|
-
])
|
|
1952
|
-
];
|
|
1953
|
-
}
|
|
1954
|
-
function buildCursorRuleFiles(targetHome, adapter, sources) {
|
|
1955
|
-
return sources.flatMap((source, index) => {
|
|
1956
|
-
const n = String(index + 1).padStart(2, "0");
|
|
1957
|
-
const stem = `${n}-${source.normalizedId}`;
|
|
1958
|
-
const relativePath = posix.join(adapter.managedDir, `${stem}.mdc`);
|
|
1959
|
-
const description = `${source.resolvedLabel} (${source.resolvedLayer})`;
|
|
1960
|
-
const content = [
|
|
1961
|
-
"---",
|
|
1962
|
-
`description: ${yamlQuote2(description)}`,
|
|
1963
|
-
'globs: ["**/*"]',
|
|
1964
|
-
"alwaysApply: true",
|
|
1965
|
-
"---",
|
|
1966
|
-
"",
|
|
1967
|
-
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1968
|
-
"",
|
|
1969
|
-
source.content.trim()
|
|
1970
|
-
].join(`
|
|
1971
|
-
`);
|
|
1972
|
-
const sourceFile = makeFile(targetHome, relativePath, "rule", content, [source.id]);
|
|
1973
|
-
const ruleFiles = source.resolvedRules.map((rule) => {
|
|
1974
|
-
const ruleStem = `${n}-${source.normalizedId}-${rule.normalizedId}`;
|
|
1975
|
-
const ruleRelativePath = posix.join(adapter.managedDir, `${ruleStem}.mdc`);
|
|
1976
|
-
const ruleDescription = `${rule.resolvedLabel} (${source.resolvedLayer})`;
|
|
1977
|
-
const ruleContent = [
|
|
1978
|
-
"---",
|
|
1979
|
-
`description: ${yamlQuote2(ruleDescription)}`,
|
|
1980
|
-
`globs: ${JSON.stringify(rule.globs && rule.globs.length > 0 ? rule.globs : ["**/*"])}`,
|
|
1981
|
-
"alwaysApply: true",
|
|
1982
|
-
"---",
|
|
1983
|
-
"",
|
|
1984
|
-
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
1985
|
-
"",
|
|
1986
|
-
rule.content.trim()
|
|
1987
|
-
].join(`
|
|
1988
|
-
`);
|
|
1989
|
-
return makeFile(targetHome, ruleRelativePath, "rule", ruleContent, [source.id, rule.id]);
|
|
1990
|
-
});
|
|
1991
|
-
return [sourceFile, ...ruleFiles];
|
|
1992
|
-
});
|
|
1993
|
-
}
|
|
1994
|
-
function buildOpenCodeFiles(targetHome, adapter, profile, sources) {
|
|
1995
|
-
const fragments = sources.flatMap((source, index) => [
|
|
1996
|
-
makeFile(targetHome, fragmentPath(adapter, index, source), "fragment", sectionForSource(source), [source.id]),
|
|
1997
|
-
...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
|
|
1998
|
-
]);
|
|
1999
|
-
const flattenedIndex = makeFile(targetHome, adapter.indexFile, "index", [
|
|
2000
|
-
indexHeader(adapter.tool, profile),
|
|
2001
|
-
...sources.flatMap((source) => [
|
|
2002
|
-
sectionForSource(source),
|
|
2003
|
-
...source.resolvedRules.map((rule) => sectionForRule(source, rule))
|
|
2004
|
-
])
|
|
2005
|
-
].join(`
|
|
2006
|
-
|
|
2007
|
-
`), [
|
|
2008
|
-
...sources.map((source) => source.id),
|
|
2009
|
-
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
2010
|
-
]);
|
|
2011
|
-
const config = {
|
|
2012
|
-
$schema: "https://opencode.ai/config.json",
|
|
2013
|
-
instructions: fragments.map((file) => file.relativePath)
|
|
1737
|
+
id: file.replace(extname(file), ""),
|
|
1738
|
+
label: file,
|
|
1739
|
+
content,
|
|
1740
|
+
layer: "agent",
|
|
1741
|
+
order,
|
|
1742
|
+
path
|
|
2014
1743
|
};
|
|
2015
|
-
return [
|
|
2016
|
-
flattenedIndex,
|
|
2017
|
-
makeFile(targetHome, adapter.configFile, "config", JSON.stringify(config, null, 2), sources.map((source) => source.id)),
|
|
2018
|
-
...fragments
|
|
2019
|
-
];
|
|
2020
1744
|
}
|
|
2021
|
-
function
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
1745
|
+
function sourceFromConfig(config, order = 0, layer) {
|
|
1746
|
+
return {
|
|
1747
|
+
id: config.slug,
|
|
1748
|
+
label: config.name,
|
|
1749
|
+
content: config.content,
|
|
1750
|
+
layer: layer ?? (config.agent === "global" ? "global" : "agent"),
|
|
1751
|
+
order,
|
|
1752
|
+
path: config.target_path ?? undefined,
|
|
1753
|
+
provenance: {
|
|
1754
|
+
source: "open-configs",
|
|
1755
|
+
configSlug: config.slug,
|
|
1756
|
+
configAgent: config.agent
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function sourcesFromIdentityExport(value, options = {}) {
|
|
1761
|
+
const record = asRecord(value, "identity instruction export");
|
|
1762
|
+
const shape = requireIdentityExportShape(record);
|
|
1763
|
+
const validation = asOptionalRecord(record["validation"]);
|
|
1764
|
+
if (validation && validation["valid"] === false) {
|
|
1765
|
+
const issues = Array.isArray(validation["issues"]) ? validation["issues"] : [];
|
|
1766
|
+
throw new Error(`Identity instruction export is invalid: ${JSON.stringify(issues)}`);
|
|
2031
1767
|
}
|
|
1768
|
+
const sources = record["sources"];
|
|
1769
|
+
if (!Array.isArray(sources))
|
|
1770
|
+
throw new Error("Identity instruction export sources must be an array.");
|
|
1771
|
+
const offset = options.orderOffset ?? 0;
|
|
1772
|
+
return sources.map((item, index) => identitySourceToSessionSource(item, {
|
|
1773
|
+
path: options.path,
|
|
1774
|
+
tool: options.tool,
|
|
1775
|
+
orderFallback: offset + index,
|
|
1776
|
+
exportShape: shape
|
|
1777
|
+
})).filter((source) => source !== null);
|
|
2032
1778
|
}
|
|
2033
|
-
function
|
|
2034
|
-
if (
|
|
2035
|
-
return
|
|
2036
|
-
|
|
2037
|
-
|
|
1779
|
+
function requireIdentityExportShape(record) {
|
|
1780
|
+
if (record["contract"] === "hasna.identities.configs-instructions/v1")
|
|
1781
|
+
return "configs-contract";
|
|
1782
|
+
if (record["version"] === 1 && record["package"] === "@hasna/identities")
|
|
1783
|
+
return "canonical-open-identities";
|
|
1784
|
+
throw new Error("Unsupported identity instruction export contract.");
|
|
2038
1785
|
}
|
|
2039
|
-
function
|
|
2040
|
-
|
|
1786
|
+
function normalizeInstructionRules(source, tool) {
|
|
1787
|
+
const seen = new Set;
|
|
1788
|
+
return (source.rules ?? []).map((rule) => {
|
|
1789
|
+
if (!rule.id.trim())
|
|
1790
|
+
throw new Error(`Instruction rule id is required for source ${source.id}.`);
|
|
1791
|
+
const content = filterProviderOnlyBlocks(rule.content ?? "", tool);
|
|
1792
|
+
if (!content.trim() && !rule.path)
|
|
1793
|
+
throw new Error(`Instruction rule content or path is required for rule ${rule.id}.`);
|
|
1794
|
+
const resolvedPath = normalizeRulePath(rule.path ?? `${slug(rule.id)}.md`);
|
|
1795
|
+
const key = resolvedPath.toLowerCase();
|
|
1796
|
+
if (seen.has(key))
|
|
1797
|
+
throw new Error(`Duplicate rule path for source ${source.id}: ${resolvedPath}`);
|
|
1798
|
+
seen.add(key);
|
|
1799
|
+
return {
|
|
1800
|
+
...rule,
|
|
1801
|
+
content,
|
|
1802
|
+
normalizedId: slug(rule.id),
|
|
1803
|
+
resolvedLabel: rule.label ?? rule.id,
|
|
1804
|
+
resolvedPath
|
|
1805
|
+
};
|
|
1806
|
+
});
|
|
2041
1807
|
}
|
|
2042
|
-
function
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
1808
|
+
function rejectDuplicateRenderPaths(files) {
|
|
1809
|
+
const seen = new Set;
|
|
1810
|
+
for (const file of files) {
|
|
1811
|
+
const key = file.relativePath.toLowerCase();
|
|
1812
|
+
if (seen.has(key))
|
|
1813
|
+
throw new Error(`Duplicate session render file path: ${file.relativePath}`);
|
|
1814
|
+
seen.add(key);
|
|
2046
1815
|
}
|
|
2047
|
-
return trimmed;
|
|
2048
1816
|
}
|
|
2049
|
-
function
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
return resolve2(home, cleaned.slice(2));
|
|
2058
|
-
if (cleaned === "{{HOME}}" || cleaned === "${HOME}")
|
|
2059
|
-
return resolve2(home);
|
|
2060
|
-
if (cleaned.startsWith("{{HOME}}/"))
|
|
2061
|
-
return resolve2(home, cleaned.slice("{{HOME}}/".length));
|
|
2062
|
-
if (cleaned.startsWith("${HOME}/"))
|
|
2063
|
-
return resolve2(home, cleaned.slice("${HOME}/".length));
|
|
2064
|
-
return resolve2(cleaned);
|
|
1817
|
+
function rejectDuplicateSourceSlugs(sources) {
|
|
1818
|
+
const seen = new Map;
|
|
1819
|
+
for (const source of sources) {
|
|
1820
|
+
const existing = seen.get(source.normalizedId);
|
|
1821
|
+
if (existing)
|
|
1822
|
+
throw new Error(`Duplicate session instruction source slug: ${source.normalizedId} (${existing}, ${source.id})`);
|
|
1823
|
+
seen.set(source.normalizedId, source.id);
|
|
1824
|
+
}
|
|
2065
1825
|
}
|
|
2066
|
-
function
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1826
|
+
function rejectDuplicateRulePaths(sources) {
|
|
1827
|
+
const seen = new Map;
|
|
1828
|
+
for (const source of sources) {
|
|
1829
|
+
for (const rule of source.resolvedRules) {
|
|
1830
|
+
const key = rule.resolvedPath.toLowerCase();
|
|
1831
|
+
const existing = seen.get(key);
|
|
1832
|
+
if (existing)
|
|
1833
|
+
throw new Error(`Duplicate instruction rule path: ${rule.resolvedPath} (${existing}, ${rule.id})`);
|
|
1834
|
+
seen.set(key, rule.id);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function normalizeRulePath(path) {
|
|
1839
|
+
if (!path.trim())
|
|
1840
|
+
throw new Error("Instruction rule path cannot be empty.");
|
|
1841
|
+
if (path.includes("\\"))
|
|
1842
|
+
throw new Error(`Instruction rule path must use POSIX separators: ${path}`);
|
|
1843
|
+
const normalized = posix.normalize(path);
|
|
2072
1844
|
if (normalized === "." || posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
|
|
2073
|
-
throw new Error(`
|
|
1845
|
+
throw new Error(`Instruction rule path escapes managed rule directory: ${path}`);
|
|
2074
1846
|
}
|
|
2075
1847
|
return normalized;
|
|
2076
1848
|
}
|
|
2077
|
-
function
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
1849
|
+
function identitySourceToSessionSource(value, options) {
|
|
1850
|
+
const record = asRecord(value, "identity instruction source");
|
|
1851
|
+
const providers = asStringArray(record["targetProviders"]);
|
|
1852
|
+
if (options.tool && providers.length > 0 && !providerTargetsTool(providers, options.tool))
|
|
1853
|
+
return null;
|
|
1854
|
+
const sourcePaths = normalizeSourcePaths(record["sourcePaths"]);
|
|
1855
|
+
const kind = maybeString(record["kind"]);
|
|
1856
|
+
const layer = record["layer"] === undefined ? layerFromIdentityKind(kind, options.exportShape) : requireLayer(record["layer"]);
|
|
1857
|
+
const merge = requireMerge(record["merge"] ?? record["mergePolicy"] ?? "append");
|
|
1858
|
+
const id = requireString(record["id"], "identity instruction source id");
|
|
1859
|
+
const inlineContent = maybeString(record["content"]);
|
|
1860
|
+
const resolvedContent = inlineContent && inlineContent.trim() ? inlineContent : contentFromIdentitySourcePaths(sourcePaths, options.path, id) ?? inlineContent;
|
|
1861
|
+
return {
|
|
1862
|
+
id,
|
|
1863
|
+
label: maybeString(record["label"]) ?? maybeString(record["title"]) ?? id,
|
|
1864
|
+
layer,
|
|
1865
|
+
merge,
|
|
1866
|
+
order: typeof record["order"] === "number" ? record["order"] : typeof record["precedence"] === "number" ? record["precedence"] : options.orderFallback,
|
|
1867
|
+
content: resolvedContent ?? "",
|
|
1868
|
+
path: options.path,
|
|
1869
|
+
rules: normalizeIdentityRules(record["rules"]),
|
|
1870
|
+
provenance: asOptionalRecord(record["provenance"]) ?? null,
|
|
1871
|
+
targetProviders: providers,
|
|
1872
|
+
owner: normalizeIdentityOwner(record["owner"]),
|
|
1873
|
+
sourcePaths,
|
|
1874
|
+
globs: asStringArray(record["globs"]),
|
|
1875
|
+
hash: maybeString(record["hash"]),
|
|
1876
|
+
nonOverridable: record["nonOverridable"] === true,
|
|
1877
|
+
replacementScope: maybeString(record["replacementScope"]),
|
|
1878
|
+
metadata: asOptionalRecord(record["metadata"]) ?? null
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function layerFromIdentityKind(kind, exportShape) {
|
|
1882
|
+
if (!kind) {
|
|
1883
|
+
if (exportShape === "configs-contract")
|
|
1884
|
+
throw new Error("Invalid session instruction layer: undefined");
|
|
1885
|
+
return "agent";
|
|
1886
|
+
}
|
|
1887
|
+
switch (kind) {
|
|
1888
|
+
case "global-rules":
|
|
1889
|
+
case "global-system-prompt":
|
|
1890
|
+
return "global";
|
|
1891
|
+
case "provider-rules":
|
|
1892
|
+
case "provider-system-prompt":
|
|
1893
|
+
return "tool";
|
|
1894
|
+
case "identity-doc":
|
|
1895
|
+
case "persona-doc":
|
|
1896
|
+
return "agent";
|
|
1897
|
+
case "account-overlay":
|
|
1898
|
+
return "account";
|
|
1899
|
+
case "project-overlay":
|
|
1900
|
+
return "repo";
|
|
1901
|
+
case "machine-overlay":
|
|
1902
|
+
return "machine";
|
|
1903
|
+
case "session-overlay":
|
|
1904
|
+
return "session";
|
|
1905
|
+
default:
|
|
1906
|
+
throw new Error(`Invalid identity instruction source kind: ${kind}`);
|
|
2083
1907
|
}
|
|
2084
|
-
return normalized;
|
|
2085
1908
|
}
|
|
2086
|
-
function
|
|
2087
|
-
if (
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
1909
|
+
function contentFromIdentitySourcePaths(sourcePaths, exportPath, sourceId) {
|
|
1910
|
+
if (sourcePaths.length === 0 || !exportPath)
|
|
1911
|
+
return;
|
|
1912
|
+
const baseDir = dirname(resolveSessionPath(exportPath));
|
|
1913
|
+
const contents = [];
|
|
1914
|
+
for (const sourcePath of sourcePaths) {
|
|
1915
|
+
const content = readIdentitySourcePath(sourcePath, baseDir, sourceId);
|
|
1916
|
+
if (content !== undefined)
|
|
1917
|
+
contents.push({ path: sourcePath.path, content });
|
|
1918
|
+
}
|
|
1919
|
+
if (contents.length === 0)
|
|
1920
|
+
return;
|
|
1921
|
+
if (contents.length === 1)
|
|
1922
|
+
return ensureTrailingNewline(contents[0].content);
|
|
1923
|
+
return ensureTrailingNewline(contents.map((item) => `<!-- Source path: ${item.path} -->
|
|
1924
|
+
${item.content.trimEnd()}`).join(`
|
|
1925
|
+
|
|
1926
|
+
`));
|
|
1927
|
+
}
|
|
1928
|
+
function readIdentitySourcePath(sourcePath, baseDir, sourceId) {
|
|
1929
|
+
const resolvedPath = resolveIdentitySourcePath(sourcePath.path, baseDir, sourceId);
|
|
1930
|
+
if (!existsSync3(resolvedPath)) {
|
|
1931
|
+
if (sourcePath.required) {
|
|
1932
|
+
throw new Error(`Required identity instruction source path not found for ${sourceId}: ${sourcePath.path}`);
|
|
2096
1933
|
}
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const stat = statSync(resolvedPath);
|
|
1937
|
+
if (!stat.isFile()) {
|
|
1938
|
+
throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
|
|
1939
|
+
}
|
|
1940
|
+
const realBase = realpathSync(baseDir);
|
|
1941
|
+
const realPath = realpathSync(resolvedPath);
|
|
1942
|
+
if (!pathIsInside(realPath, realBase)) {
|
|
1943
|
+
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
|
|
1944
|
+
}
|
|
1945
|
+
return readFileSync(realPath, "utf-8");
|
|
1946
|
+
}
|
|
1947
|
+
function resolveIdentitySourcePath(path, baseDir, sourceId) {
|
|
1948
|
+
const cleaned = cleanSessionPathInput(path);
|
|
1949
|
+
if (!cleaned)
|
|
1950
|
+
throw new Error(`Identity instruction source path cannot be empty for ${sourceId}.`);
|
|
1951
|
+
if (cleaned.includes("\\"))
|
|
1952
|
+
throw new Error(`Identity instruction source path must use POSIX separators for ${sourceId}: ${path}`);
|
|
1953
|
+
const resolvedPath = isAbsolute(cleaned) ? resolve(cleaned) : resolve(baseDir, cleaned);
|
|
1954
|
+
if (!pathIsInside(resolvedPath, resolve(baseDir))) {
|
|
1955
|
+
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${path}`);
|
|
1956
|
+
}
|
|
1957
|
+
return resolvedPath;
|
|
1958
|
+
}
|
|
1959
|
+
function pathIsInside(path, baseDir) {
|
|
1960
|
+
const rel = relative(baseDir, path);
|
|
1961
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1962
|
+
}
|
|
1963
|
+
function providerTargetsTool(targets, tool) {
|
|
1964
|
+
return targets.map((target) => target.toLowerCase()).some((target) => target === tool || target === "all" || target === "generic");
|
|
1965
|
+
}
|
|
1966
|
+
function normalizeIdentityRules(value) {
|
|
1967
|
+
if (value === undefined || value === null)
|
|
1968
|
+
return [];
|
|
1969
|
+
if (!Array.isArray(value))
|
|
1970
|
+
throw new Error("Identity instruction source rules must be an array.");
|
|
1971
|
+
return value.map((item) => {
|
|
1972
|
+
const record = asRecord(item, "identity instruction rule");
|
|
1973
|
+
const id = requireString(record["id"], "identity instruction rule id");
|
|
2097
1974
|
return {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
1975
|
+
id,
|
|
1976
|
+
label: maybeString(record["label"]) ?? id,
|
|
1977
|
+
path: maybeString(record["path"]),
|
|
1978
|
+
content: maybeString(record["content"]) ?? "",
|
|
1979
|
+
globs: asStringArray(record["globs"]),
|
|
1980
|
+
hash: maybeString(record["hash"]),
|
|
1981
|
+
metadata: asOptionalRecord(record["metadata"]) ?? null
|
|
2101
1982
|
};
|
|
2102
|
-
}
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
function normalizeIdentityOwner(value) {
|
|
1986
|
+
if (value === undefined || value === null)
|
|
1987
|
+
return null;
|
|
1988
|
+
const record = asRecord(value, "identity instruction owner");
|
|
2103
1989
|
return {
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
blockers: []
|
|
1990
|
+
kind: requireString(record["kind"], "identity instruction owner kind"),
|
|
1991
|
+
id: requireString(record["id"], "identity instruction owner id")
|
|
2107
1992
|
};
|
|
2108
1993
|
}
|
|
2109
|
-
function
|
|
2110
|
-
if (
|
|
2111
|
-
return
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
projectRoot: input.projectRoot ? resolveSessionPath(input.projectRoot) : null,
|
|
2117
|
-
ownedBy: "open-configs",
|
|
2118
|
-
reason: "target resolution blocked before provider files can be owned"
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
if (target.targetKind === "project-root") {
|
|
1994
|
+
function normalizeSourcePaths(value) {
|
|
1995
|
+
if (value === undefined || value === null)
|
|
1996
|
+
return [];
|
|
1997
|
+
if (!Array.isArray(value))
|
|
1998
|
+
throw new Error("Identity instruction source paths must be an array.");
|
|
1999
|
+
return value.map((item) => {
|
|
2000
|
+
const record = asRecord(item, "identity instruction source path");
|
|
2122
2001
|
return {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
projectRoot: target.targetHome,
|
|
2128
|
-
ownedBy: "open-configs",
|
|
2129
|
-
reason: "project-scoped provider files are generated in the explicit repository root"
|
|
2002
|
+
path: requireString(record["path"], "identity instruction source path"),
|
|
2003
|
+
editable: record["editable"] === true,
|
|
2004
|
+
required: record["required"] === true,
|
|
2005
|
+
hash: maybeString(record["hash"])
|
|
2130
2006
|
};
|
|
2131
|
-
}
|
|
2132
|
-
return {
|
|
2133
|
-
kind: "provider-profile",
|
|
2134
|
-
tool: input.tool,
|
|
2135
|
-
profile: input.profile,
|
|
2136
|
-
targetHome: target.targetHome,
|
|
2137
|
-
projectRoot: null,
|
|
2138
|
-
ownedBy: "open-configs",
|
|
2139
|
-
reason: "profile-scoped provider home is generated by OpenConfigs from identity/config sources"
|
|
2140
|
-
};
|
|
2007
|
+
});
|
|
2141
2008
|
}
|
|
2142
|
-
function
|
|
2143
|
-
|
|
2144
|
-
throw new Error(`Unsupported session render tool: ${input.tool}`);
|
|
2145
|
-
if (!input.profile.trim())
|
|
2146
|
-
throw new Error("Session render profile is required.");
|
|
2147
|
-
const adapter = adapterFor(input);
|
|
2148
|
-
const { targetHome, targetKind, blockers } = resolveRenderTarget(input);
|
|
2149
|
-
const targetOwner = resolveSessionTargetOwnership(input, { targetHome, targetKind });
|
|
2150
|
-
const blocked = blockers.length > 0;
|
|
2151
|
-
const allowEmptySources = input.allowEmptySources === true;
|
|
2152
|
-
const orderedSources = composeSources(normalizeSources(input.sources, input.tool, allowEmptySources));
|
|
2153
|
-
if (orderedSources.length === 0 && !allowEmptySources) {
|
|
2154
|
-
throw new Error("Session render has no instruction sources. Pass --allow-empty-sources only for explicit empty renders.");
|
|
2155
|
-
}
|
|
2156
|
-
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
2157
|
-
const env = adapter.envVar && !blocked ? { [adapter.envVar]: targetHome } : {};
|
|
2158
|
-
const warnings = [
|
|
2159
|
-
...orderedSources.length === 0 ? ["No instruction sources were provided."] : [],
|
|
2160
|
-
...blockers
|
|
2161
|
-
];
|
|
2162
|
-
const files = blocked ? [] : buildFiles(targetHome, adapter, input.profile, orderedSources);
|
|
2163
|
-
rejectDuplicateRenderPaths(files);
|
|
2164
|
-
const manifest = {
|
|
2165
|
-
schema: SESSION_RENDER_SCHEMA,
|
|
2166
|
-
tool: input.tool,
|
|
2167
|
-
adapterMode: adapter.mode,
|
|
2168
|
-
profile: input.profile,
|
|
2169
|
-
sessionId: input.sessionId ?? null,
|
|
2170
|
-
targetHome,
|
|
2171
|
-
targetKind,
|
|
2172
|
-
targetOwner,
|
|
2173
|
-
writable: !blocked,
|
|
2174
|
-
blocked,
|
|
2175
|
-
blockers,
|
|
2176
|
-
generatedAt,
|
|
2177
|
-
env,
|
|
2178
|
-
sourceHash: fingerprint(orderedSources.map((source) => ({
|
|
2179
|
-
id: source.id,
|
|
2180
|
-
layer: source.resolvedLayer,
|
|
2181
|
-
order: source.resolvedOrder,
|
|
2182
|
-
merge: source.resolvedMerge,
|
|
2183
|
-
content: source.content,
|
|
2184
|
-
rules: source.resolvedRules.map((rule) => ({ id: rule.id, path: rule.resolvedPath, content: rule.content })),
|
|
2185
|
-
hash: source.hash ?? null
|
|
2186
|
-
}))),
|
|
2187
|
-
sources: orderedSources.map((source) => ({
|
|
2188
|
-
id: source.id,
|
|
2189
|
-
label: source.resolvedLabel,
|
|
2190
|
-
layer: source.resolvedLayer,
|
|
2191
|
-
merge: source.resolvedMerge,
|
|
2192
|
-
order: source.resolvedOrder,
|
|
2193
|
-
path: source.path ?? null,
|
|
2194
|
-
targetProviders: source.targetProviders ?? [],
|
|
2195
|
-
owner: source.owner ?? null,
|
|
2196
|
-
sourcePaths: source.sourcePaths ?? [],
|
|
2197
|
-
hash: source.hash ?? null,
|
|
2198
|
-
nonOverridable: source.nonOverridable === true,
|
|
2199
|
-
replacementScope: source.replacementScope ?? null,
|
|
2200
|
-
rules: source.resolvedRules.map((rule) => ({
|
|
2201
|
-
id: rule.id,
|
|
2202
|
-
label: rule.resolvedLabel,
|
|
2203
|
-
path: rule.resolvedPath,
|
|
2204
|
-
globs: rule.globs ?? [],
|
|
2205
|
-
hash: rule.hash ?? null
|
|
2206
|
-
})),
|
|
2207
|
-
provenance: source.provenance ?? null
|
|
2208
|
-
})),
|
|
2209
|
-
skippedSources: [],
|
|
2210
|
-
files: files.map((file) => ({
|
|
2211
|
-
path: file.path,
|
|
2212
|
-
relativePath: file.relativePath,
|
|
2213
|
-
role: file.role,
|
|
2214
|
-
sha256: file.sha256,
|
|
2215
|
-
sourceIds: file.sourceIds
|
|
2216
|
-
})),
|
|
2217
|
-
warnings
|
|
2218
|
-
};
|
|
2219
|
-
const manifestFile = makeFile(targetHome, posix.join(".hasna", "session-render-manifest.json"), "manifest", JSON.stringify(manifest, null, 2), orderedSources.map((source) => source.id));
|
|
2220
|
-
return {
|
|
2221
|
-
dryRun: true,
|
|
2222
|
-
tool: input.tool,
|
|
2223
|
-
adapter,
|
|
2224
|
-
profile: input.profile,
|
|
2225
|
-
sessionId: input.sessionId ?? null,
|
|
2226
|
-
targetHome,
|
|
2227
|
-
targetKind,
|
|
2228
|
-
targetOwner,
|
|
2229
|
-
writable: !blocked,
|
|
2230
|
-
blocked,
|
|
2231
|
-
blockers,
|
|
2232
|
-
env,
|
|
2233
|
-
files,
|
|
2234
|
-
manifest,
|
|
2235
|
-
manifestFile,
|
|
2236
|
-
allFiles: [...files, manifestFile],
|
|
2237
|
-
warnings
|
|
2238
|
-
};
|
|
2009
|
+
function requireLayer(value) {
|
|
2010
|
+
return normalizeSessionInstructionLayer(value);
|
|
2239
2011
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
label: file,
|
|
2245
|
-
content,
|
|
2246
|
-
layer: "agent",
|
|
2247
|
-
order,
|
|
2248
|
-
path
|
|
2249
|
-
};
|
|
2012
|
+
function requireMerge(value) {
|
|
2013
|
+
if (value === "append" || value === "replace")
|
|
2014
|
+
return value;
|
|
2015
|
+
throw new Error(`Invalid session instruction merge policy: ${String(value)}`);
|
|
2250
2016
|
}
|
|
2251
|
-
function
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2017
|
+
function asRecord(value, label) {
|
|
2018
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2019
|
+
throw new Error(`Invalid ${label}.`);
|
|
2020
|
+
return value;
|
|
2021
|
+
}
|
|
2022
|
+
function asOptionalRecord(value) {
|
|
2023
|
+
if (value === undefined || value === null)
|
|
2024
|
+
return null;
|
|
2025
|
+
return asRecord(value, "record");
|
|
2026
|
+
}
|
|
2027
|
+
function maybeString(value) {
|
|
2028
|
+
return typeof value === "string" ? value : undefined;
|
|
2029
|
+
}
|
|
2030
|
+
function requireString(value, label) {
|
|
2031
|
+
if (typeof value !== "string" || !value.trim())
|
|
2032
|
+
throw new Error(`Invalid ${label}.`);
|
|
2033
|
+
return value;
|
|
2034
|
+
}
|
|
2035
|
+
function asStringArray(value) {
|
|
2036
|
+
if (value === undefined || value === null)
|
|
2037
|
+
return [];
|
|
2038
|
+
if (!Array.isArray(value))
|
|
2039
|
+
return [];
|
|
2040
|
+
return value.filter((item) => typeof item === "string");
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/lib/transforms.ts
|
|
2044
|
+
import { basename as basename2, extname as extname2 } from "path";
|
|
2045
|
+
function ensureTrailingNewline2(content) {
|
|
2046
|
+
return content.endsWith(`
|
|
2047
|
+
`) ? content : `${content}
|
|
2048
|
+
`;
|
|
2049
|
+
}
|
|
2050
|
+
function yamlQuote2(value) {
|
|
2051
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2052
|
+
}
|
|
2053
|
+
function headingLevel(line) {
|
|
2054
|
+
const m = line.match(/^(#{1,6})\s+/);
|
|
2055
|
+
return m ? m[1].length : null;
|
|
2056
|
+
}
|
|
2057
|
+
function stripClaudeOnlySections(content) {
|
|
2058
|
+
const withoutMarkedBlocks = content.replace(/<!--\s*claude-only:start\s*-->[\s\S]*?<!--\s*claude-only:end\s*-->/gi, "");
|
|
2059
|
+
const lines = withoutMarkedBlocks.split(`
|
|
2060
|
+
`);
|
|
2061
|
+
const kept = [];
|
|
2062
|
+
let droppingUntilLevel = null;
|
|
2063
|
+
for (const line of lines) {
|
|
2064
|
+
const level = headingLevel(line);
|
|
2065
|
+
if (level !== null && droppingUntilLevel !== null && level <= droppingUntilLevel) {
|
|
2066
|
+
droppingUntilLevel = null;
|
|
2263
2067
|
}
|
|
2264
|
-
|
|
2068
|
+
if (droppingUntilLevel !== null)
|
|
2069
|
+
continue;
|
|
2070
|
+
if (/^#{1,6}\s+.*claude(?:\s+code)?[-\s]+only\b/i.test(line)) {
|
|
2071
|
+
droppingUntilLevel = level ?? 1;
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
kept.push(line);
|
|
2075
|
+
}
|
|
2076
|
+
return kept.join(`
|
|
2077
|
+
`).replace(/\n{3,}/g, `
|
|
2078
|
+
|
|
2079
|
+
`).trim();
|
|
2080
|
+
}
|
|
2081
|
+
function isClaudeRuleConfig(source, candidate) {
|
|
2082
|
+
if (candidate.id === source.id)
|
|
2083
|
+
return false;
|
|
2084
|
+
if (candidate.agent !== "claude" || candidate.category !== "rules")
|
|
2085
|
+
return false;
|
|
2086
|
+
return !!candidate.target_path?.includes("/rules/");
|
|
2265
2087
|
}
|
|
2266
|
-
function
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2088
|
+
function ruleLabel(config) {
|
|
2089
|
+
const file = config.target_path ? basename2(config.target_path) : config.name;
|
|
2090
|
+
return file.replace(/\.(md|mdc|markdown)$/i, "");
|
|
2091
|
+
}
|
|
2092
|
+
function claudeRules(source, context) {
|
|
2093
|
+
return (context.configs ?? []).filter((config) => isClaudeRuleConfig(source, config)).sort((a, b) => (a.target_path ?? a.name).localeCompare(b.target_path ?? b.name));
|
|
2094
|
+
}
|
|
2095
|
+
function flattenWithRules(source, context) {
|
|
2096
|
+
const parts = [stripClaudeOnlySections(source.content)];
|
|
2097
|
+
const rules = claudeRules(source, context).map((rule) => ({ rule, content: stripClaudeOnlySections(rule.content) })).filter(({ content }) => Boolean(content)).map(({ rule, content }) => `### ${ruleLabel(rule)}
|
|
2098
|
+
|
|
2099
|
+
${content}`);
|
|
2100
|
+
if (rules.length > 0) {
|
|
2101
|
+
parts.push(`## Rules
|
|
2102
|
+
|
|
2103
|
+
${rules.join(`
|
|
2104
|
+
|
|
2105
|
+
`)}`);
|
|
2273
2106
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
const offset = options.orderOffset ?? 0;
|
|
2278
|
-
return sources.map((item, index) => identitySourceToSessionSource(item, {
|
|
2279
|
-
path: options.path,
|
|
2280
|
-
tool: options.tool,
|
|
2281
|
-
orderFallback: offset + index,
|
|
2282
|
-
exportShape: shape
|
|
2283
|
-
})).filter((source) => source !== null);
|
|
2107
|
+
return ensureTrailingNewline2(parts.filter(Boolean).join(`
|
|
2108
|
+
|
|
2109
|
+
`));
|
|
2284
2110
|
}
|
|
2285
|
-
function
|
|
2286
|
-
|
|
2287
|
-
return "configs-contract";
|
|
2288
|
-
if (record["version"] === 1 && record["package"] === "@hasna/identities")
|
|
2289
|
-
return "canonical-open-identities";
|
|
2290
|
-
throw new Error("Unsupported identity instruction export contract.");
|
|
2111
|
+
function buildCodexAgentsMd(source, context = {}) {
|
|
2112
|
+
return flattenWithRules(source, context);
|
|
2291
2113
|
}
|
|
2292
|
-
function
|
|
2293
|
-
|
|
2294
|
-
return (source.rules ?? []).map((rule) => {
|
|
2295
|
-
if (!rule.id.trim())
|
|
2296
|
-
throw new Error(`Instruction rule id is required for source ${source.id}.`);
|
|
2297
|
-
const content = filterProviderOnlyBlocks(rule.content ?? "", tool);
|
|
2298
|
-
if (!content.trim() && !rule.path)
|
|
2299
|
-
throw new Error(`Instruction rule content or path is required for rule ${rule.id}.`);
|
|
2300
|
-
const resolvedPath = normalizeRulePath(rule.path ?? `${slug(rule.id)}.md`);
|
|
2301
|
-
const key = resolvedPath.toLowerCase();
|
|
2302
|
-
if (seen.has(key))
|
|
2303
|
-
throw new Error(`Duplicate rule path for source ${source.id}: ${resolvedPath}`);
|
|
2304
|
-
seen.add(key);
|
|
2305
|
-
return {
|
|
2306
|
-
...rule,
|
|
2307
|
-
content,
|
|
2308
|
-
normalizedId: slug(rule.id),
|
|
2309
|
-
resolvedLabel: rule.label ?? rule.id,
|
|
2310
|
-
resolvedPath
|
|
2311
|
-
};
|
|
2312
|
-
});
|
|
2114
|
+
function buildOpenCodeAgentsMd(source, context = {}) {
|
|
2115
|
+
return flattenWithRules(source, context);
|
|
2313
2116
|
}
|
|
2314
|
-
function
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2117
|
+
function buildCursorMdc(source) {
|
|
2118
|
+
const stem = source.target_path ? basename2(source.target_path, extname2(source.target_path)) : source.slug;
|
|
2119
|
+
const description = source.name || stem;
|
|
2120
|
+
return ensureTrailingNewline2([
|
|
2121
|
+
"---",
|
|
2122
|
+
`description: ${yamlQuote2(description)}`,
|
|
2123
|
+
'globs: ["**/*"]',
|
|
2124
|
+
"alwaysApply: true",
|
|
2125
|
+
"---",
|
|
2126
|
+
"",
|
|
2127
|
+
stripClaudeOnlySections(source.content)
|
|
2128
|
+
].join(`
|
|
2129
|
+
`));
|
|
2130
|
+
}
|
|
2131
|
+
function transformSkillContent(content) {
|
|
2132
|
+
return ensureTrailingNewline2(content.split(`
|
|
2133
|
+
`).filter((line) => !/^\s*user_invocable\s*:/i.test(line)).join(`
|
|
2134
|
+
`).replace(/\bAgent tool\b/g, "delegate through the available MCP or agent orchestration tools").replace(/\bTodoWrite\b/g, "track tasks with the agent's native task mechanism").replace(/\bRead\/Edit\b/g, "read and edit files with the available filesystem tools").replace(/\bRead\b/g, "read files with the available filesystem tools").replace(/\bEdit\b/g, "edit files with the available filesystem tools").replace(/\bBash\b/g, "run shell commands with the available terminal tool").trim());
|
|
2135
|
+
}
|
|
2136
|
+
function applyTransform(source, output, context = {}) {
|
|
2137
|
+
switch (output.transform) {
|
|
2138
|
+
case "passthrough":
|
|
2139
|
+
case "claude-passthrough":
|
|
2140
|
+
return ensureTrailingNewline2(source.content);
|
|
2141
|
+
case "codex-flat":
|
|
2142
|
+
return buildCodexAgentsMd(source, context);
|
|
2143
|
+
case "opencode-flat":
|
|
2144
|
+
return buildOpenCodeAgentsMd(source, context);
|
|
2145
|
+
case "cursor-mdc":
|
|
2146
|
+
return buildCursorMdc(source);
|
|
2147
|
+
case "skill-neutral":
|
|
2148
|
+
return transformSkillContent(source.content);
|
|
2321
2149
|
}
|
|
2322
2150
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2151
|
+
|
|
2152
|
+
// src/lib/apply.ts
|
|
2153
|
+
function getConfigHome() {
|
|
2154
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir3();
|
|
2155
|
+
}
|
|
2156
|
+
function expandPath(p) {
|
|
2157
|
+
if (p.startsWith("~/")) {
|
|
2158
|
+
return resolve2(getConfigHome(), p.slice(2));
|
|
2330
2159
|
}
|
|
2160
|
+
return resolve2(p);
|
|
2331
2161
|
}
|
|
2332
|
-
function
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2162
|
+
function normalizeTargetPath(p) {
|
|
2163
|
+
const expanded = expandPath(p);
|
|
2164
|
+
try {
|
|
2165
|
+
return realpathSync2(expanded);
|
|
2166
|
+
} catch {
|
|
2167
|
+
let current = expanded;
|
|
2168
|
+
const missingSegments = [];
|
|
2169
|
+
while (true) {
|
|
2170
|
+
if (existsSync4(current)) {
|
|
2171
|
+
try {
|
|
2172
|
+
return resolve2(realpathSync2(current), ...missingSegments);
|
|
2173
|
+
} catch {
|
|
2174
|
+
return expanded;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const parent = dirname2(current);
|
|
2178
|
+
const name = basename3(current);
|
|
2179
|
+
if (parent === current)
|
|
2180
|
+
return expanded;
|
|
2181
|
+
missingSegments.unshift(name);
|
|
2182
|
+
current = parent;
|
|
2341
2183
|
}
|
|
2342
2184
|
}
|
|
2343
2185
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2186
|
+
async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
2187
|
+
const renderedTargetPath = opts.vars ? renderMachineAwareContent(targetPath, opts.vars) : targetPath;
|
|
2188
|
+
const renderedContent = opts.vars ? renderMachineAwareContent(content, opts.vars) : content;
|
|
2189
|
+
const targetAgent = meta.agent ?? config.agent;
|
|
2190
|
+
if (isAntigravityRuleTarget(targetAgent, renderedTargetPath) && renderedContent.length > ANTIGRAVITY_RULE_FILE_CHAR_LIMIT) {
|
|
2191
|
+
throw new ConfigApplyError(`Antigravity rule file ${renderedTargetPath} is ${renderedContent.length} characters; split it before applying because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
|
|
2192
|
+
}
|
|
2193
|
+
const path = expandPath(renderedTargetPath);
|
|
2194
|
+
const previousContent = existsSync4(path) ? readFileSync2(path, "utf-8") : null;
|
|
2195
|
+
const changed = previousContent !== renderedContent;
|
|
2196
|
+
if (!opts.dryRun) {
|
|
2197
|
+
const dir = dirname2(path);
|
|
2198
|
+
if (!existsSync4(dir)) {
|
|
2199
|
+
mkdirSync2(dir, { recursive: true });
|
|
2200
|
+
}
|
|
2201
|
+
if (previousContent !== null && changed) {
|
|
2202
|
+
const store = opts.store ?? resolveConfigStore();
|
|
2203
|
+
await store.createSnapshot(config.id, previousContent, config.version);
|
|
2204
|
+
}
|
|
2205
|
+
writeFileSync(path, renderedContent, "utf-8");
|
|
2352
2206
|
}
|
|
2353
|
-
return normalized;
|
|
2354
|
-
}
|
|
2355
|
-
function identitySourceToSessionSource(value, options) {
|
|
2356
|
-
const record = asRecord(value, "identity instruction source");
|
|
2357
|
-
const providers = asStringArray(record["targetProviders"]);
|
|
2358
|
-
if (options.tool && providers.length > 0 && !providerTargetsTool(providers, options.tool))
|
|
2359
|
-
return null;
|
|
2360
|
-
const sourcePaths = normalizeSourcePaths(record["sourcePaths"]);
|
|
2361
|
-
const kind = maybeString(record["kind"]);
|
|
2362
|
-
const layer = record["layer"] === undefined ? layerFromIdentityKind(kind, options.exportShape) : requireLayer(record["layer"]);
|
|
2363
|
-
const merge = requireMerge(record["merge"] ?? record["mergePolicy"] ?? "append");
|
|
2364
|
-
const id = requireString(record["id"], "identity instruction source id");
|
|
2365
|
-
const inlineContent = maybeString(record["content"]);
|
|
2366
|
-
const resolvedContent = inlineContent && inlineContent.trim() ? inlineContent : contentFromIdentitySourcePaths(sourcePaths, options.path, id) ?? inlineContent;
|
|
2367
2207
|
return {
|
|
2368
|
-
id,
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
rules: normalizeIdentityRules(record["rules"]),
|
|
2376
|
-
provenance: asOptionalRecord(record["provenance"]) ?? null,
|
|
2377
|
-
targetProviders: providers,
|
|
2378
|
-
owner: normalizeIdentityOwner(record["owner"]),
|
|
2379
|
-
sourcePaths,
|
|
2380
|
-
globs: asStringArray(record["globs"]),
|
|
2381
|
-
hash: maybeString(record["hash"]),
|
|
2382
|
-
nonOverridable: record["nonOverridable"] === true,
|
|
2383
|
-
replacementScope: maybeString(record["replacementScope"]),
|
|
2384
|
-
metadata: asOptionalRecord(record["metadata"]) ?? null
|
|
2208
|
+
config_id: config.id,
|
|
2209
|
+
path,
|
|
2210
|
+
previous_content: previousContent,
|
|
2211
|
+
new_content: renderedContent,
|
|
2212
|
+
dry_run: opts.dryRun ?? false,
|
|
2213
|
+
changed,
|
|
2214
|
+
...meta
|
|
2385
2215
|
};
|
|
2386
2216
|
}
|
|
2387
|
-
function
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2217
|
+
function isAntigravityRuleTarget(agent, targetPath) {
|
|
2218
|
+
return agent === "antigravity" && /\.(md|mdc|markdown)$/i.test(targetPath);
|
|
2219
|
+
}
|
|
2220
|
+
function isGeneratedOutputTarget(config, configs) {
|
|
2221
|
+
if (!config.target_path)
|
|
2222
|
+
return false;
|
|
2223
|
+
const targetPath = normalizeTargetPath(config.target_path);
|
|
2224
|
+
return configs.some((candidate) => candidate.id !== config.id && candidate.outputs.some((output) => normalizeTargetPath(output.target_path) === targetPath));
|
|
2225
|
+
}
|
|
2226
|
+
async function applyConfig(config, opts = {}) {
|
|
2227
|
+
if (opts.outputAgent && isRetiredOrUnsupportedConfigAgent(opts.outputAgent)) {
|
|
2228
|
+
throw new ConfigApplyError(`Config output agent "${opts.outputAgent}" is retired or unsupported \u2014 cannot apply to disk.`);
|
|
2392
2229
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2230
|
+
if (isRetiredOrUnsupportedConfigAgent(config.agent)) {
|
|
2231
|
+
throw new ConfigApplyError(`Config "${config.name}" uses retired or unsupported agent "${config.agent}" \u2014 cannot apply to disk.`);
|
|
2232
|
+
}
|
|
2233
|
+
const selectedOutputs = opts.outputAgent ? config.outputs.filter((output) => output.agent === opts.outputAgent) : config.outputs.filter((output) => !isRetiredOrUnsupportedConfigAgent(output.agent));
|
|
2234
|
+
const shouldApplyPrimary = !opts.outputAgent || config.agent === opts.outputAgent;
|
|
2235
|
+
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
2236
|
+
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
2237
|
+
}
|
|
2238
|
+
const store = opts.store ?? resolveConfigStore();
|
|
2239
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
2240
|
+
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
2241
|
+
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
2242
|
+
}
|
|
2243
|
+
const outputResults = [];
|
|
2244
|
+
for (const output of selectedOutputs) {
|
|
2245
|
+
outputResults.push(await applyOutput(config, output, contextConfigs, opts));
|
|
2246
|
+
}
|
|
2247
|
+
let result;
|
|
2248
|
+
if (config.target_path && shouldApplyPrimary) {
|
|
2249
|
+
result = await writeConfigResult(config, config.target_path, config.content, opts);
|
|
2250
|
+
result.outputs = outputResults;
|
|
2251
|
+
result.changed = result.changed || outputResults.some((output) => output.changed);
|
|
2252
|
+
} else {
|
|
2253
|
+
result = {
|
|
2254
|
+
...outputResults[0],
|
|
2255
|
+
outputs: outputResults.slice(1),
|
|
2256
|
+
changed: outputResults.some((output) => output.changed)
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
if (!opts.dryRun) {
|
|
2260
|
+
await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
|
|
2412
2261
|
}
|
|
2262
|
+
return result;
|
|
2413
2263
|
}
|
|
2414
|
-
function
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2264
|
+
async function applyOutput(config, output, contextConfigs, opts) {
|
|
2265
|
+
const content = applyTransform(config, output, { configs: contextConfigs });
|
|
2266
|
+
return writeConfigResult(config, output.target_path, content, opts, {
|
|
2267
|
+
agent: output.agent,
|
|
2268
|
+
transform: output.transform
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
async function applyConfigs(configs, opts = {}) {
|
|
2272
|
+
const results = [];
|
|
2273
|
+
for (const config of configs) {
|
|
2274
|
+
if (config.kind === "reference")
|
|
2275
|
+
continue;
|
|
2276
|
+
if (isRetiredOrUnsupportedConfigAgent(config.agent))
|
|
2277
|
+
continue;
|
|
2278
|
+
results.push(await applyConfig(config, opts));
|
|
2423
2279
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
if (contents.length === 1)
|
|
2427
|
-
return ensureTrailingNewline2(contents[0].content);
|
|
2428
|
-
return ensureTrailingNewline2(contents.map((item) => `<!-- Source path: ${item.path} -->
|
|
2429
|
-
${item.content.trimEnd()}`).join(`
|
|
2280
|
+
return results;
|
|
2281
|
+
}
|
|
2430
2282
|
|
|
2431
|
-
|
|
2283
|
+
// src/lib/package-version.ts
|
|
2284
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
2285
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
2286
|
+
import { fileURLToPath } from "url";
|
|
2287
|
+
var cached = null;
|
|
2288
|
+
function getPackageVersion() {
|
|
2289
|
+
if (cached)
|
|
2290
|
+
return cached;
|
|
2291
|
+
try {
|
|
2292
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
2293
|
+
for (let i = 0;i < 8; i++) {
|
|
2294
|
+
const pkgPath = join4(dir, "package.json");
|
|
2295
|
+
if (existsSync5(pkgPath)) {
|
|
2296
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
2297
|
+
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
2298
|
+
cached = pkg.version;
|
|
2299
|
+
return cached;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const parent = dirname3(dir);
|
|
2303
|
+
if (parent === dir)
|
|
2304
|
+
break;
|
|
2305
|
+
dir = parent;
|
|
2306
|
+
}
|
|
2307
|
+
} catch {}
|
|
2308
|
+
cached = "0.0.0";
|
|
2309
|
+
return cached;
|
|
2432
2310
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2311
|
+
|
|
2312
|
+
// src/lib/redact.ts
|
|
2313
|
+
var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
|
|
2314
|
+
var VALUE_PATTERNS = [
|
|
2315
|
+
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
2316
|
+
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
2317
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
2318
|
+
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
2319
|
+
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
2320
|
+
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
2321
|
+
{ re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
|
|
2322
|
+
{ re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
|
|
2323
|
+
];
|
|
2324
|
+
var MIN_SECRET_VALUE_LEN = 8;
|
|
2325
|
+
function redactShell(content) {
|
|
2326
|
+
const redacted = [];
|
|
2327
|
+
const lines = content.split(`
|
|
2328
|
+
`);
|
|
2329
|
+
const out = [];
|
|
2330
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2331
|
+
const line = lines[i];
|
|
2332
|
+
const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
2333
|
+
if (m) {
|
|
2334
|
+
const [, prefix, key, eq, quote, value] = m;
|
|
2335
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2336
|
+
const reason = reasonFor(key, value);
|
|
2337
|
+
redacted.push({ varName: key, line: i + 1, reason });
|
|
2338
|
+
out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2438
2341
|
}
|
|
2439
|
-
|
|
2440
|
-
}
|
|
2441
|
-
const stat = statSync(resolvedPath);
|
|
2442
|
-
if (!stat.isFile()) {
|
|
2443
|
-
throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
|
|
2444
|
-
}
|
|
2445
|
-
const realBase = realpathSync2(baseDir);
|
|
2446
|
-
const realPath = realpathSync2(resolvedPath);
|
|
2447
|
-
if (!pathIsInside(realPath, realBase)) {
|
|
2448
|
-
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
|
|
2342
|
+
out.push(line);
|
|
2449
2343
|
}
|
|
2450
|
-
return
|
|
2344
|
+
return { content: out.join(`
|
|
2345
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2451
2346
|
}
|
|
2452
|
-
function
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2347
|
+
function redactJson(content) {
|
|
2348
|
+
const redacted = [];
|
|
2349
|
+
const lines = content.split(`
|
|
2350
|
+
`);
|
|
2351
|
+
const out = [];
|
|
2352
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2353
|
+
const line = lines[i];
|
|
2354
|
+
const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
|
|
2355
|
+
if (m) {
|
|
2356
|
+
const [, prefix, key, value, comma, trail] = m;
|
|
2357
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2358
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2359
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2360
|
+
out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
let newLine = line;
|
|
2365
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2366
|
+
newLine = newLine.replace(re, (match) => {
|
|
2367
|
+
const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
2368
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
2369
|
+
return `{{${varName}}}`;
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
out.push(newLine);
|
|
2461
2373
|
}
|
|
2462
|
-
return
|
|
2374
|
+
return { content: out.join(`
|
|
2375
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2463
2376
|
}
|
|
2464
|
-
function
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2377
|
+
function redactToml(content) {
|
|
2378
|
+
const redacted = [];
|
|
2379
|
+
const lines = content.split(`
|
|
2380
|
+
`);
|
|
2381
|
+
const out = [];
|
|
2382
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2383
|
+
const line = lines[i];
|
|
2384
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
2385
|
+
if (m) {
|
|
2386
|
+
const [, indent, key, eq, quote, value] = m;
|
|
2387
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2388
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2389
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2390
|
+
out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
out.push(line);
|
|
2395
|
+
}
|
|
2396
|
+
return { content: out.join(`
|
|
2397
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2467
2398
|
}
|
|
2468
|
-
function
|
|
2469
|
-
|
|
2399
|
+
function redactIni(content) {
|
|
2400
|
+
const redacted = [];
|
|
2401
|
+
const lines = content.split(`
|
|
2402
|
+
`);
|
|
2403
|
+
const out = [];
|
|
2404
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2405
|
+
const line = lines[i];
|
|
2406
|
+
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
2407
|
+
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
2408
|
+
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
2409
|
+
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
2413
|
+
if (m) {
|
|
2414
|
+
const [, indent, key, eq, value] = m;
|
|
2415
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2416
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2417
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2418
|
+
out.push(`${indent}${key}${eq}{{${varName}}}`);
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
out.push(line);
|
|
2423
|
+
}
|
|
2424
|
+
return { content: out.join(`
|
|
2425
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2470
2426
|
}
|
|
2471
|
-
function
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2427
|
+
function redactGeneric(content) {
|
|
2428
|
+
const redacted = [];
|
|
2429
|
+
const lines = content.split(`
|
|
2430
|
+
`);
|
|
2431
|
+
const out = [];
|
|
2432
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2433
|
+
let line = lines[i];
|
|
2434
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2435
|
+
line = line.replace(re, (match) => {
|
|
2436
|
+
const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2437
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
2438
|
+
return `{{${varName}}}`;
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
out.push(line);
|
|
2442
|
+
}
|
|
2443
|
+
return { content: out.join(`
|
|
2444
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2489
2445
|
}
|
|
2490
|
-
function
|
|
2491
|
-
if (value
|
|
2492
|
-
return
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2446
|
+
function shouldRedactKeyValue(key, value) {
|
|
2447
|
+
if (!value || value.startsWith("{{"))
|
|
2448
|
+
return false;
|
|
2449
|
+
if (isReferenceValue(value.trim()))
|
|
2450
|
+
return false;
|
|
2451
|
+
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
2452
|
+
return false;
|
|
2453
|
+
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
2454
|
+
return false;
|
|
2455
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
2456
|
+
return true;
|
|
2457
|
+
for (const { re } of VALUE_PATTERNS) {
|
|
2458
|
+
if (re.test(value))
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
return false;
|
|
2498
2462
|
}
|
|
2499
|
-
function
|
|
2500
|
-
if (
|
|
2501
|
-
return
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
path: requireString(record["path"], "identity instruction source path"),
|
|
2508
|
-
editable: record["editable"] === true,
|
|
2509
|
-
required: record["required"] === true,
|
|
2510
|
-
hash: maybeString(record["hash"])
|
|
2511
|
-
};
|
|
2512
|
-
});
|
|
2463
|
+
function reasonFor(key, value) {
|
|
2464
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
2465
|
+
return `secret key name: ${key}`;
|
|
2466
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2467
|
+
if (re.test(value))
|
|
2468
|
+
return reason;
|
|
2469
|
+
}
|
|
2470
|
+
return "secret value pattern";
|
|
2513
2471
|
}
|
|
2514
|
-
function
|
|
2515
|
-
|
|
2516
|
-
return value;
|
|
2517
|
-
throw new Error(`Invalid session instruction layer: ${String(value)}`);
|
|
2472
|
+
function isReferenceValue(value) {
|
|
2473
|
+
return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
|
|
2518
2474
|
}
|
|
2519
|
-
function
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2475
|
+
function redactContent(content, format) {
|
|
2476
|
+
switch (format) {
|
|
2477
|
+
case "shell":
|
|
2478
|
+
return redactShell(content);
|
|
2479
|
+
case "json":
|
|
2480
|
+
return redactJson(content);
|
|
2481
|
+
case "toml":
|
|
2482
|
+
return redactToml(content);
|
|
2483
|
+
case "ini":
|
|
2484
|
+
return redactIni(content);
|
|
2485
|
+
default:
|
|
2486
|
+
return redactGeneric(content);
|
|
2487
|
+
}
|
|
2523
2488
|
}
|
|
2524
|
-
function
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
return value;
|
|
2489
|
+
function scanSecrets(content, format) {
|
|
2490
|
+
const r = redactContent(content, format);
|
|
2491
|
+
return r.redacted;
|
|
2528
2492
|
}
|
|
2529
|
-
function
|
|
2530
|
-
|
|
2531
|
-
return null;
|
|
2532
|
-
return asRecord(value, "record");
|
|
2493
|
+
function hasSecrets(content, format) {
|
|
2494
|
+
return scanSecrets(content, format).length > 0;
|
|
2533
2495
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2496
|
+
|
|
2497
|
+
// src/status.ts
|
|
2498
|
+
var PACKAGE_NAME = "@hasna/instructions";
|
|
2499
|
+
var PACKAGE_VERSION = getPackageVersion();
|
|
2500
|
+
function activeDatabaseEnv() {
|
|
2501
|
+
if (process.env["HASNA_INSTRUCTIONS_DB_PATH"])
|
|
2502
|
+
return "HASNA_INSTRUCTIONS_DB_PATH";
|
|
2503
|
+
return null;
|
|
2536
2504
|
}
|
|
2537
|
-
function
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
return value;
|
|
2505
|
+
function configuredDatabaseKind() {
|
|
2506
|
+
const value = process.env["HASNA_INSTRUCTIONS_DB_PATH"] ?? "";
|
|
2507
|
+
return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
|
|
2541
2508
|
}
|
|
2542
|
-
function
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2509
|
+
function countBy(items, getValue) {
|
|
2510
|
+
const counts = {};
|
|
2511
|
+
for (const item of items) {
|
|
2512
|
+
const value = getValue(item);
|
|
2513
|
+
if (!value)
|
|
2514
|
+
continue;
|
|
2515
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
2516
|
+
}
|
|
2517
|
+
return counts;
|
|
2518
|
+
}
|
|
2519
|
+
async function getConfigsStatus(store = resolveConfigStore()) {
|
|
2520
|
+
let databaseReachable = true;
|
|
2521
|
+
let configs = [];
|
|
2522
|
+
let categoryStats = { total: 0 };
|
|
2523
|
+
try {
|
|
2524
|
+
configs = await store.listConfigs();
|
|
2525
|
+
categoryStats = await store.getConfigStats();
|
|
2526
|
+
} catch {
|
|
2527
|
+
databaseReachable = false;
|
|
2528
|
+
}
|
|
2529
|
+
const fileConfigs = configs.filter((config) => config.kind === "file");
|
|
2530
|
+
const retiredAgentRows = configs.filter((config) => isRetiredOrUnsupportedConfigAgent(config.agent)).length;
|
|
2531
|
+
let driftedTargets = 0;
|
|
2532
|
+
let missingTargets = 0;
|
|
2533
|
+
let unredactedSecretFindings = 0;
|
|
2534
|
+
let knownTargets = 0;
|
|
2535
|
+
for (const config of fileConfigs) {
|
|
2536
|
+
unredactedSecretFindings += scanSecrets(config.content, config.format).length;
|
|
2537
|
+
if (isRetiredOrUnsupportedConfigAgent(config.agent))
|
|
2538
|
+
continue;
|
|
2539
|
+
if (!config.target_path)
|
|
2540
|
+
continue;
|
|
2541
|
+
knownTargets += 1;
|
|
2542
|
+
const targetPath = expandPath(config.target_path);
|
|
2543
|
+
if (!existsSync6(targetPath)) {
|
|
2544
|
+
missingTargets += 1;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const disk = readFileSync4(targetPath, "utf-8");
|
|
2548
|
+
const { content: redactedDisk } = redactContent(disk, config.format);
|
|
2549
|
+
if (redactedDisk !== config.content) {
|
|
2550
|
+
driftedTargets += 1;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
let profiles = 0;
|
|
2554
|
+
let machines = 0;
|
|
2555
|
+
let profileLinks = 0;
|
|
2556
|
+
let snapshots = 0;
|
|
2557
|
+
if (databaseReachable) {
|
|
2558
|
+
try {
|
|
2559
|
+
const profileList = await store.listProfiles();
|
|
2560
|
+
profiles = profileList.length;
|
|
2561
|
+
machines = (await store.listMachines()).length;
|
|
2562
|
+
for (const profile of profileList) {
|
|
2563
|
+
profileLinks += (await store.getProfileConfigs(profile.id)).length;
|
|
2564
|
+
}
|
|
2565
|
+
for (const config of configs) {
|
|
2566
|
+
snapshots += (await store.listSnapshots(config.id)).length;
|
|
2567
|
+
}
|
|
2568
|
+
} catch {
|
|
2569
|
+
databaseReachable = false;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
|
|
2573
|
+
const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 && retiredAgentRows === 0 ? "ok" : "warn";
|
|
2574
|
+
return {
|
|
2575
|
+
service: "configs",
|
|
2576
|
+
schemaVersion: "1.0",
|
|
2577
|
+
package: {
|
|
2578
|
+
name: PACKAGE_NAME,
|
|
2579
|
+
version: PACKAGE_VERSION
|
|
2580
|
+
},
|
|
2581
|
+
env: {
|
|
2582
|
+
database: {
|
|
2583
|
+
primary: "HASNA_INSTRUCTIONS_DB_PATH",
|
|
2584
|
+
active: activeDatabaseEnv(),
|
|
2585
|
+
kind: configuredDatabaseKind()
|
|
2586
|
+
}
|
|
2587
|
+
},
|
|
2588
|
+
counts: {
|
|
2589
|
+
configs: {
|
|
2590
|
+
total: configs.length,
|
|
2591
|
+
file: fileConfigs.length,
|
|
2592
|
+
reference: configs.filter((config) => config.kind === "reference").length,
|
|
2593
|
+
templates: configs.filter((config) => config.is_template).length,
|
|
2594
|
+
retiredAgentRows
|
|
2595
|
+
},
|
|
2596
|
+
byCategory,
|
|
2597
|
+
byAgent: countBy(configs, (config) => config.agent),
|
|
2598
|
+
byFormat: countBy(configs, (config) => config.format),
|
|
2599
|
+
profiles,
|
|
2600
|
+
profileLinks,
|
|
2601
|
+
machines,
|
|
2602
|
+
snapshots,
|
|
2603
|
+
knownTargets
|
|
2604
|
+
},
|
|
2605
|
+
health: {
|
|
2606
|
+
status,
|
|
2607
|
+
databaseReachable,
|
|
2608
|
+
driftedTargets,
|
|
2609
|
+
missingTargets,
|
|
2610
|
+
unredactedSecretFindings,
|
|
2611
|
+
retiredAgentRows,
|
|
2612
|
+
hasDrift: driftedTargets > 0,
|
|
2613
|
+
hasMissingTargets: missingTargets > 0,
|
|
2614
|
+
hasUnredactedSecrets: unredactedSecretFindings > 0,
|
|
2615
|
+
hasRetiredAgentRows: retiredAgentRows > 0
|
|
2616
|
+
},
|
|
2617
|
+
safety: {
|
|
2618
|
+
includesConfigValues: false,
|
|
2619
|
+
includesPrivatePaths: false,
|
|
2620
|
+
includesHostnames: false,
|
|
2621
|
+
includesSecretValues: false,
|
|
2622
|
+
statusOutputIsMetadataOnly: true
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2548
2625
|
}
|
|
2626
|
+
// src/db/pg-migrations.ts
|
|
2627
|
+
var PG_MIGRATIONS = [
|
|
2628
|
+
`CREATE TABLE IF NOT EXISTS configs (
|
|
2629
|
+
id TEXT PRIMARY KEY,
|
|
2630
|
+
name TEXT NOT NULL,
|
|
2631
|
+
slug TEXT NOT NULL UNIQUE,
|
|
2632
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
2633
|
+
category TEXT NOT NULL,
|
|
2634
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
2635
|
+
target_path TEXT,
|
|
2636
|
+
outputs TEXT NOT NULL DEFAULT '[]',
|
|
2637
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
2638
|
+
content TEXT NOT NULL DEFAULT '',
|
|
2639
|
+
description TEXT,
|
|
2640
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
2641
|
+
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
2642
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
2643
|
+
created_at TEXT NOT NULL,
|
|
2644
|
+
updated_at TEXT NOT NULL,
|
|
2645
|
+
synced_at TEXT
|
|
2646
|
+
)`,
|
|
2647
|
+
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
2648
|
+
id TEXT PRIMARY KEY,
|
|
2649
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
2650
|
+
content TEXT NOT NULL,
|
|
2651
|
+
version INTEGER NOT NULL,
|
|
2652
|
+
created_at TEXT NOT NULL
|
|
2653
|
+
)`,
|
|
2654
|
+
`CREATE TABLE IF NOT EXISTS profiles (
|
|
2655
|
+
id TEXT PRIMARY KEY,
|
|
2656
|
+
name TEXT NOT NULL,
|
|
2657
|
+
slug TEXT NOT NULL UNIQUE,
|
|
2658
|
+
description TEXT,
|
|
2659
|
+
selectors TEXT NOT NULL DEFAULT '{}',
|
|
2660
|
+
variables TEXT NOT NULL DEFAULT '{}',
|
|
2661
|
+
created_at TEXT NOT NULL,
|
|
2662
|
+
updated_at TEXT NOT NULL
|
|
2663
|
+
)`,
|
|
2664
|
+
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
2665
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
2666
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
2667
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
2668
|
+
PRIMARY KEY (profile_id, config_id)
|
|
2669
|
+
)`,
|
|
2670
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
2671
|
+
id TEXT PRIMARY KEY,
|
|
2672
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
2673
|
+
os TEXT,
|
|
2674
|
+
arch TEXT,
|
|
2675
|
+
last_applied_at TEXT,
|
|
2676
|
+
created_at TEXT NOT NULL
|
|
2677
|
+
)`,
|
|
2678
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
2679
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
2680
|
+
message TEXT NOT NULL,
|
|
2681
|
+
email TEXT,
|
|
2682
|
+
category TEXT DEFAULT 'general',
|
|
2683
|
+
version TEXT,
|
|
2684
|
+
machine_id TEXT,
|
|
2685
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
2686
|
+
)`,
|
|
2687
|
+
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
2688
|
+
];
|
|
2549
2689
|
// src/lib/session-apply.ts
|
|
2550
2690
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
2551
2691
|
import {
|
|
2552
|
-
existsSync as
|
|
2692
|
+
existsSync as existsSync7,
|
|
2553
2693
|
lstatSync,
|
|
2554
2694
|
mkdirSync as mkdirSync3,
|
|
2555
|
-
readFileSync as
|
|
2695
|
+
readFileSync as readFileSync5,
|
|
2556
2696
|
renameSync,
|
|
2557
2697
|
rmSync as rmSync2,
|
|
2558
2698
|
writeFileSync as writeFileSync2
|
|
2559
2699
|
} from "fs";
|
|
2560
|
-
import { dirname as
|
|
2700
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
|
|
2561
2701
|
class SessionApplyError extends Error {
|
|
2562
2702
|
constructor(message) {
|
|
2563
2703
|
super(message);
|
|
@@ -2598,7 +2738,7 @@ function applySessionRender(plan, options = {}) {
|
|
|
2598
2738
|
snapshotPath = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest);
|
|
2599
2739
|
for (const file of files) {
|
|
2600
2740
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
2601
|
-
const existingContent =
|
|
2741
|
+
const existingContent = existsSync7(target) ? readFileSync5(target, "utf-8") : null;
|
|
2602
2742
|
if (existingContent === file.content)
|
|
2603
2743
|
continue;
|
|
2604
2744
|
writePlannedFile(target, file.content, targetHome);
|
|
@@ -2607,7 +2747,7 @@ function applySessionRender(plan, options = {}) {
|
|
|
2607
2747
|
if (result.action !== "delete")
|
|
2608
2748
|
continue;
|
|
2609
2749
|
assertNoSymlinkSegments(targetHome, result.path);
|
|
2610
|
-
if (
|
|
2750
|
+
if (existsSync7(result.path))
|
|
2611
2751
|
rmSync2(result.path);
|
|
2612
2752
|
}
|
|
2613
2753
|
}
|
|
@@ -2642,7 +2782,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
2642
2782
|
const drifted = [];
|
|
2643
2783
|
for (const file of previousManifest.files) {
|
|
2644
2784
|
const target = resolveManifestRelativePath(file.relativePath, safeTargetHome);
|
|
2645
|
-
if (!
|
|
2785
|
+
if (!existsSync7(target)) {
|
|
2646
2786
|
missing.push({
|
|
2647
2787
|
path: target,
|
|
2648
2788
|
relativePath: file.relativePath,
|
|
@@ -2652,7 +2792,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
2652
2792
|
});
|
|
2653
2793
|
continue;
|
|
2654
2794
|
}
|
|
2655
|
-
const actualSha256 = sha2562(
|
|
2795
|
+
const actualSha256 = sha2562(readFileSync5(target, "utf-8"));
|
|
2656
2796
|
if (actualSha256 !== file.sha256) {
|
|
2657
2797
|
drifted.push({
|
|
2658
2798
|
path: target,
|
|
@@ -2674,7 +2814,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
2674
2814
|
}
|
|
2675
2815
|
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
2676
2816
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
2677
|
-
const previousContent =
|
|
2817
|
+
const previousContent = existsSync7(target) ? readFileSync5(target, "utf-8") : null;
|
|
2678
2818
|
const previousSha256 = previousContent === null ? null : sha2562(previousContent);
|
|
2679
2819
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
2680
2820
|
const changed = previousContent !== file.content;
|
|
@@ -2757,9 +2897,9 @@ function planStaleFileResults(plan, targetHome, previousManifest, currentRelativ
|
|
|
2757
2897
|
}
|
|
2758
2898
|
function planStaleFileResult(file, targetHome, options) {
|
|
2759
2899
|
const target = resolveManifestRelativePath(file.relativePath, targetHome);
|
|
2760
|
-
if (!
|
|
2900
|
+
if (!existsSync7(target))
|
|
2761
2901
|
return null;
|
|
2762
|
-
const previousContent =
|
|
2902
|
+
const previousContent = readFileSync5(target, "utf-8");
|
|
2763
2903
|
const previousSha256 = sha2562(previousContent);
|
|
2764
2904
|
if (!options.force && previousSha256 !== file.sha256) {
|
|
2765
2905
|
return {
|
|
@@ -2825,10 +2965,10 @@ function resolveManifestRelativePath(relativePath, targetHome) {
|
|
|
2825
2965
|
return target;
|
|
2826
2966
|
}
|
|
2827
2967
|
function readPreviousManifest(path) {
|
|
2828
|
-
if (!
|
|
2968
|
+
if (!existsSync7(path))
|
|
2829
2969
|
return null;
|
|
2830
2970
|
try {
|
|
2831
|
-
const parsed = JSON.parse(
|
|
2971
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
2832
2972
|
if (parsed.schema !== SESSION_RENDER_SCHEMA)
|
|
2833
2973
|
return null;
|
|
2834
2974
|
if (!Array.isArray(parsed.files))
|
|
@@ -2839,16 +2979,16 @@ function readPreviousManifest(path) {
|
|
|
2839
2979
|
}
|
|
2840
2980
|
}
|
|
2841
2981
|
function writePlannedFile(path, content, targetHome) {
|
|
2842
|
-
const dir =
|
|
2982
|
+
const dir = dirname4(path);
|
|
2843
2983
|
mkdirSync3(dir, { recursive: true });
|
|
2844
2984
|
assertNoSymlinkSegments(targetHome, path);
|
|
2845
|
-
const tmp =
|
|
2985
|
+
const tmp = join5(dir, `.session-${randomUUID3()}.tmp`);
|
|
2846
2986
|
writeFileSync2(tmp, content, "utf-8");
|
|
2847
2987
|
renameSync(tmp, path);
|
|
2848
2988
|
}
|
|
2849
2989
|
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest) {
|
|
2850
|
-
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) =>
|
|
2851
|
-
const content =
|
|
2990
|
+
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync7(result.path)).map((result) => {
|
|
2991
|
+
const content = readFileSync5(result.path, "utf-8");
|
|
2852
2992
|
return {
|
|
2853
2993
|
path: result.path,
|
|
2854
2994
|
relativePath: result.relativePath,
|
|
@@ -2883,7 +3023,7 @@ function assertSafeTargetHome(targetHome) {
|
|
|
2883
3023
|
throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
|
|
2884
3024
|
}
|
|
2885
3025
|
assertNoSymlinkAncestors(normalized);
|
|
2886
|
-
if (
|
|
3026
|
+
if (existsSync7(normalized) && lstatSync(normalized).isSymbolicLink()) {
|
|
2887
3027
|
throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
|
|
2888
3028
|
}
|
|
2889
3029
|
return normalized;
|
|
@@ -2893,8 +3033,8 @@ function assertNoSymlinkSegments(root, target) {
|
|
|
2893
3033
|
const rel = relative2(root, target);
|
|
2894
3034
|
let current = root;
|
|
2895
3035
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
2896
|
-
current =
|
|
2897
|
-
if (
|
|
3036
|
+
current = join5(current, segment);
|
|
3037
|
+
if (existsSync7(current) && lstatSync(current).isSymbolicLink()) {
|
|
2898
3038
|
throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
|
|
2899
3039
|
}
|
|
2900
3040
|
}
|
|
@@ -2905,8 +3045,8 @@ function assertNoSymlinkAncestors(path) {
|
|
|
2905
3045
|
let current = parsed.root;
|
|
2906
3046
|
const rel = relative2(parsed.root, normalized);
|
|
2907
3047
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
2908
|
-
current =
|
|
2909
|
-
if (!
|
|
3048
|
+
current = join5(current, segment);
|
|
3049
|
+
if (!existsSync7(current))
|
|
2910
3050
|
return;
|
|
2911
3051
|
if (lstatSync(current).isSymbolicLink()) {
|
|
2912
3052
|
throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
|
|
@@ -3071,13 +3211,86 @@ async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
|
3071
3211
|
}
|
|
3072
3212
|
return ensured;
|
|
3073
3213
|
}
|
|
3214
|
+
// src/lib/global-agent-rules-standard.ts
|
|
3215
|
+
var GLOBAL_AGENT_RULES_STANDARD_SLUG = "global-agent-rules-standard";
|
|
3216
|
+
var GLOBAL_AGENT_RULES_STANDARD_CONTENT = `# Global Coding Agent Rules Standard
|
|
3217
|
+
|
|
3218
|
+
This standard is managed global/system prompt source content for Hasna coding
|
|
3219
|
+
agents. Rendered agents must receive these rules unless a newer authorized
|
|
3220
|
+
policy source supersedes them.
|
|
3221
|
+
|
|
3222
|
+
## Session and Planning Defaults
|
|
3223
|
+
|
|
3224
|
+
1. Use automatic session renaming when the agent supports it. Rename the
|
|
3225
|
+
session early to match the active task; if the task materially pivots, rename
|
|
3226
|
+
it again so operators can identify the run.
|
|
3227
|
+
2. Never set Codewith goal/token budgets or goal-plan budgets unless the user
|
|
3228
|
+
explicitly asks for a budget. Durable goals and goal plans are unbudgeted by
|
|
3229
|
+
default.
|
|
3230
|
+
|
|
3231
|
+
## Repository Mutation and Landing
|
|
3232
|
+
|
|
3233
|
+
3. Repo mutation must happen in a task-scoped worktree. First inspect the canonical worktree root
|
|
3234
|
+
\`$HOME/.hasna/repos/worktrees\`; prefer Hasna repo/project worktree
|
|
3235
|
+
mechanisms when available; otherwise use \`git worktree\`. Never mutate shared checkouts.
|
|
3236
|
+
4. PR-first landing: normal changes go through a branch/worktree and pull
|
|
3237
|
+
request before landing.
|
|
3238
|
+
5. Never push directly to \`main\`, the default branch, or any protected branch
|
|
3239
|
+
unless the user explicitly instructs that exact repo and operation.
|
|
3240
|
+
|
|
3241
|
+
## Autonomy and Source-of-Truth Tools
|
|
3242
|
+
|
|
3243
|
+
6. Act autonomously. Diagnose, repair, validate, and iterate on the owning
|
|
3244
|
+
CLIs, packages, and workflows before asking the user. Ask only when blocked
|
|
3245
|
+
by destructive decisions, secret-bearing decisions, user-only authority, or
|
|
3246
|
+
external state the agent cannot safely obtain.
|
|
3247
|
+
7. Use Hasna CLIs/packages as the source of truth: \`todos\`, \`conversations\`,
|
|
3248
|
+
\`mementos\`, \`knowledge\`, \`projects\`, \`repos\`, \`accounts\`,
|
|
3249
|
+
\`instructions\`, \`machines\`, \`secrets\`, and \`access\`.
|
|
3250
|
+
8. Secrets safety is mandatory. Never expose secrets in prompts, tasks,
|
|
3251
|
+
memories, conversations, manifests, reports, logs, PR text, or any other
|
|
3252
|
+
agent-visible output. Reference vault item names, secret identifiers, and
|
|
3253
|
+
access grants only; never print credential values.
|
|
3254
|
+
|
|
3255
|
+
## Conversation Surfaces
|
|
3256
|
+
|
|
3257
|
+
9. Use default conversation surfaces correctly: \`announcements\` for policy,
|
|
3258
|
+
freeze, breaking, cutover, and release notices; \`incidents\` for outages,
|
|
3259
|
+
crash loops, data risk, or security exposure; \`git-publishing\` before and
|
|
3260
|
+
after package publishes; \`git-prs\`, \`git-commits\`, and \`git-releases\`
|
|
3261
|
+
for repository landing events; \`hq\` for broad coordination;
|
|
3262
|
+
\`agent-policy\` for agent operating-rule discussion; project/product
|
|
3263
|
+
channels for normal work; and \`conversations blockers\` for blocker
|
|
3264
|
+
discovery. Do not invent or refer to a literal blockers channel.
|
|
3265
|
+
`;
|
|
3266
|
+
async function ensureGlobalAgentRulesStandardConfig(store = resolveConfigStore()) {
|
|
3267
|
+
const input = {
|
|
3268
|
+
name: "Global Agent Rules Standard",
|
|
3269
|
+
category: "rules",
|
|
3270
|
+
agent: "global",
|
|
3271
|
+
format: "markdown",
|
|
3272
|
+
content: GLOBAL_AGENT_RULES_STANDARD_CONTENT,
|
|
3273
|
+
kind: "reference",
|
|
3274
|
+
description: "Managed global/system prompt rules for Hasna coding agents",
|
|
3275
|
+
tags: ["global-agent-rules", "system-prompt", "coding-agent-rules"]
|
|
3276
|
+
};
|
|
3277
|
+
try {
|
|
3278
|
+
const existing = await store.getConfig(GLOBAL_AGENT_RULES_STANDARD_SLUG);
|
|
3279
|
+
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind || JSON.stringify(existing.tags) !== JSON.stringify(input.tags)) {
|
|
3280
|
+
return await store.updateConfig(existing.id, input);
|
|
3281
|
+
}
|
|
3282
|
+
return existing;
|
|
3283
|
+
} catch {
|
|
3284
|
+
return await store.createConfig(input);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3074
3287
|
// src/lib/sync.ts
|
|
3075
|
-
import { existsSync as
|
|
3076
|
-
import { basename as basename4, extname as extname3, join as
|
|
3288
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
3289
|
+
import { basename as basename4, extname as extname3, join as join7 } from "path";
|
|
3077
3290
|
|
|
3078
3291
|
// src/lib/sync-dir.ts
|
|
3079
|
-
import { existsSync as
|
|
3080
|
-
import { join as
|
|
3292
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
3293
|
+
import { join as join6, relative as relative3 } from "path";
|
|
3081
3294
|
import { homedir as homedir4 } from "os";
|
|
3082
3295
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
3083
3296
|
function shouldSkip(p) {
|
|
@@ -3086,9 +3299,9 @@ function shouldSkip(p) {
|
|
|
3086
3299
|
async function syncFromDir(dir, opts = {}) {
|
|
3087
3300
|
const store = opts.store ?? resolveConfigStore();
|
|
3088
3301
|
const absDir = expandPath(dir);
|
|
3089
|
-
if (!
|
|
3302
|
+
if (!existsSync8(absDir))
|
|
3090
3303
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
3091
|
-
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) =>
|
|
3304
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join6(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
3092
3305
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3093
3306
|
const home = homedir4();
|
|
3094
3307
|
const allConfigs = await store.listConfigs();
|
|
@@ -3098,7 +3311,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
3098
3311
|
continue;
|
|
3099
3312
|
}
|
|
3100
3313
|
try {
|
|
3101
|
-
const content =
|
|
3314
|
+
const content = readFileSync6(file, "utf-8");
|
|
3102
3315
|
if (content.length > 500000) {
|
|
3103
3316
|
result.skipped.push(file + " (too large)");
|
|
3104
3317
|
continue;
|
|
@@ -3143,7 +3356,7 @@ async function syncToDir(dir, opts = {}) {
|
|
|
3143
3356
|
}
|
|
3144
3357
|
function walkDir(dir, files = []) {
|
|
3145
3358
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
3146
|
-
const full =
|
|
3359
|
+
const full = join6(dir, entry.name);
|
|
3147
3360
|
if (shouldSkip(full))
|
|
3148
3361
|
continue;
|
|
3149
3362
|
if (entry.isDirectory())
|
|
@@ -3159,7 +3372,8 @@ var CLAUDE_PROMPT_OUTPUTS = [
|
|
|
3159
3372
|
{ agent: "codex", target_path: "~/.codex/AGENTS.md", transform: "codex-flat" },
|
|
3160
3373
|
{ agent: "codewith", target_path: "~/.codewith/CODEWITH.md", transform: "codex-flat" },
|
|
3161
3374
|
{ agent: "opencode", target_path: "~/.config/opencode/AGENTS.md", transform: "opencode-flat" },
|
|
3162
|
-
{ agent: "aicopilot", target_path: "~/.config/aicopilot/
|
|
3375
|
+
{ agent: "aicopilot", target_path: "~/.config/aicopilot/AICOPILOT.md", transform: "codex-flat" },
|
|
3376
|
+
{ agent: "antigravity", target_path: "~/.gemini/GEMINI.md", transform: "codex-flat" },
|
|
3163
3377
|
{ agent: "cursor", target_path: "~/.cursor/rules/claude.mdc", transform: "cursor-mdc" }
|
|
3164
3378
|
];
|
|
3165
3379
|
function claudeRuleOutputs(fileName) {
|
|
@@ -3193,7 +3407,7 @@ function isGeneratedOutputTarget2(config, owners) {
|
|
|
3193
3407
|
return !!ownerIds && !ownerIds.has(config.id);
|
|
3194
3408
|
}
|
|
3195
3409
|
function hasClaudePromptSource() {
|
|
3196
|
-
return
|
|
3410
|
+
return existsSync9(expandPath("~/.claude/CLAUDE.md"));
|
|
3197
3411
|
}
|
|
3198
3412
|
function hasClaudeRuleSourceForCursorTarget(targetPath) {
|
|
3199
3413
|
const absoluteTargetPath = expandPath(targetPath);
|
|
@@ -3201,7 +3415,7 @@ function hasClaudeRuleSourceForCursorTarget(targetPath) {
|
|
|
3201
3415
|
if (!absoluteTargetPath.startsWith(`${absolutePrefix}/`) || !absoluteTargetPath.endsWith(".mdc"))
|
|
3202
3416
|
return false;
|
|
3203
3417
|
const stem = basename4(absoluteTargetPath, ".mdc");
|
|
3204
|
-
return
|
|
3418
|
+
return existsSync9(expandPath(`~/.claude/rules/${stem}.md`)) || existsSync9(expandPath(`~/.claude/rules/${stem}.mdc`));
|
|
3205
3419
|
}
|
|
3206
3420
|
function isKnownGeneratedTargetPath(targetPath) {
|
|
3207
3421
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
@@ -3224,10 +3438,10 @@ var KNOWN_CONFIGS = [
|
|
|
3224
3438
|
{ path: "~/.cursor/mcp.json", name: "cursor-mcp", category: "mcp", agent: "cursor", format: "json", optional: true, description: "Cursor MCP config (includes Skills MCP server entries)" },
|
|
3225
3439
|
{ path: "~/.codewith/CODEWITH.md", name: "codewith-codewith-md", category: "rules", agent: "codewith", format: "markdown", optional: true },
|
|
3226
3440
|
{ path: "~/.codewith/config.toml", name: "codewith-config", category: "mcp", agent: "codewith", format: "toml", optional: true, description: "codewith config (Codex fork, includes Skills MCP server entries)" },
|
|
3227
|
-
{ path: "~/.config/aicopilot/
|
|
3228
|
-
{ path: "~/.config/aicopilot/
|
|
3229
|
-
{ path: "~/.gemini/
|
|
3230
|
-
{ path: "~/.gemini/
|
|
3441
|
+
{ path: "~/.config/aicopilot/AICOPILOT.md", name: "aicopilot-aicopilot-md", category: "rules", agent: "aicopilot", format: "markdown", optional: true },
|
|
3442
|
+
{ path: "~/.config/aicopilot/aicopilot.json", name: "aicopilot-config", category: "mcp", agent: "aicopilot", format: "json", optional: true, description: "AI Copilot config (includes instructions and MCP server entries)" },
|
|
3443
|
+
{ path: "~/.gemini/GEMINI.md", name: "antigravity-global-rules", category: "rules", agent: "antigravity", format: "markdown", optional: true, description: "Google Antigravity global rules file" },
|
|
3444
|
+
{ path: "~/.gemini/config/mcp_config.json", name: "antigravity-global-mcp", category: "mcp", agent: "antigravity", format: "json", optional: true, description: "Google Antigravity global MCP server entries" },
|
|
3231
3445
|
{ path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
|
|
3232
3446
|
{ path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
|
|
3233
3447
|
{ path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh", optional: true },
|
|
@@ -3245,10 +3459,12 @@ var PROJECT_CONFIG_FILES = [
|
|
|
3245
3459
|
{ file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
|
|
3246
3460
|
{ file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
|
|
3247
3461
|
{ file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
|
|
3248
|
-
{ file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" },
|
|
3249
3462
|
{ file: ".opencode/AGENTS.md", category: "rules", agent: "opencode", format: "markdown" },
|
|
3250
3463
|
{ file: ".codewith/CODEWITH.md", category: "rules", agent: "codewith", format: "markdown" },
|
|
3251
|
-
{ file: ".
|
|
3464
|
+
{ file: ".aicopilot/AICOPILOT.md", category: "rules", agent: "aicopilot", format: "markdown" },
|
|
3465
|
+
{ file: "AICOPILOT.md", category: "rules", agent: "aicopilot", format: "markdown" },
|
|
3466
|
+
{ file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" },
|
|
3467
|
+
{ file: ".agents/mcp_config.json", category: "mcp", agent: "antigravity", format: "json" }
|
|
3252
3468
|
];
|
|
3253
3469
|
async function syncProject(opts) {
|
|
3254
3470
|
const store = opts.store ?? resolveConfigStore();
|
|
@@ -3258,11 +3474,11 @@ async function syncProject(opts) {
|
|
|
3258
3474
|
const allConfigs = await store.listConfigs();
|
|
3259
3475
|
const machine = detectMachineContext();
|
|
3260
3476
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
3261
|
-
const abs =
|
|
3262
|
-
if (!
|
|
3477
|
+
const abs = join7(absDir, pf.file);
|
|
3478
|
+
if (!existsSync9(abs))
|
|
3263
3479
|
continue;
|
|
3264
3480
|
try {
|
|
3265
|
-
const rawContent =
|
|
3481
|
+
const rawContent = readFileSync7(abs, "utf-8");
|
|
3266
3482
|
if (rawContent.length > 500000) {
|
|
3267
3483
|
result.skipped.push(pf.file);
|
|
3268
3484
|
continue;
|
|
@@ -3290,23 +3506,27 @@ async function syncProject(opts) {
|
|
|
3290
3506
|
result.skipped.push(pf.file);
|
|
3291
3507
|
}
|
|
3292
3508
|
}
|
|
3293
|
-
const
|
|
3294
|
-
|
|
3295
|
-
|
|
3509
|
+
for (const ruleDir of [
|
|
3510
|
+
{ dir: join7(absDir, ".claude", "rules"), agent: "claude", namePrefix: "rules" },
|
|
3511
|
+
{ dir: join7(absDir, ".agents", "rules"), agent: "antigravity", namePrefix: "antigravity-rules" }
|
|
3512
|
+
]) {
|
|
3513
|
+
if (!existsSync9(ruleDir.dir))
|
|
3514
|
+
continue;
|
|
3515
|
+
const mdFiles = readdirSync2(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
3296
3516
|
for (const f of mdFiles) {
|
|
3297
|
-
const abs =
|
|
3298
|
-
const raw =
|
|
3517
|
+
const abs = join7(ruleDir.dir, f);
|
|
3518
|
+
const raw = readFileSync7(abs, "utf-8");
|
|
3299
3519
|
const redacted = redactContent(raw, "markdown");
|
|
3300
3520
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
3301
3521
|
const content = machineAware.content;
|
|
3302
3522
|
const isTemplate2 = redacted.isTemplate || machineAware.changed;
|
|
3303
|
-
const name = `${projectName}
|
|
3523
|
+
const name = `${projectName}/${ruleDir.namePrefix}/${f}`;
|
|
3304
3524
|
const targetPath = abs.startsWith(getConfigHome()) ? abs.replace(getConfigHome(), "~") : abs;
|
|
3305
3525
|
const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
3306
3526
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
|
|
3307
3527
|
if (!existing) {
|
|
3308
3528
|
if (!opts.dryRun)
|
|
3309
|
-
await store.createConfig({ name, category: "rules", agent:
|
|
3529
|
+
await store.createConfig({ name, category: "rules", agent: ruleDir.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
|
|
3310
3530
|
result.added++;
|
|
3311
3531
|
} else if (existing.content !== content) {
|
|
3312
3532
|
if (!opts.dryRun)
|
|
@@ -3334,20 +3554,20 @@ async function syncKnown(opts = {}) {
|
|
|
3334
3554
|
for (const known of targets) {
|
|
3335
3555
|
if (known.rulesDir) {
|
|
3336
3556
|
const absDir = expandPath(known.rulesDir);
|
|
3337
|
-
if (!
|
|
3557
|
+
if (!existsSync9(absDir)) {
|
|
3338
3558
|
result.skipped.push(known.rulesDir);
|
|
3339
3559
|
continue;
|
|
3340
3560
|
}
|
|
3341
3561
|
const extensions = known.rulesExtensions ?? [".md", ".mdc"];
|
|
3342
3562
|
const ruleFiles = readdirSync2(absDir).filter((f) => extensions.some((ext) => f.endsWith(ext)));
|
|
3343
3563
|
for (const f of ruleFiles) {
|
|
3344
|
-
const abs2 =
|
|
3564
|
+
const abs2 = join7(absDir, f);
|
|
3345
3565
|
const targetPath = abs2.replace(home, "~");
|
|
3346
3566
|
if (existingOutputOwners.has(normalizeTargetPath(targetPath)) || isKnownGeneratedTargetPath(targetPath)) {
|
|
3347
3567
|
result.skipped.push(`${targetPath} (generated output)`);
|
|
3348
3568
|
continue;
|
|
3349
3569
|
}
|
|
3350
|
-
const raw =
|
|
3570
|
+
const raw = readFileSync7(abs2, "utf-8");
|
|
3351
3571
|
const redacted = redactContent(raw, "markdown");
|
|
3352
3572
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
3353
3573
|
const content = machineAware.content;
|
|
@@ -3375,12 +3595,12 @@ async function syncKnown(opts = {}) {
|
|
|
3375
3595
|
continue;
|
|
3376
3596
|
}
|
|
3377
3597
|
const abs = expandPath(known.path);
|
|
3378
|
-
if (!
|
|
3598
|
+
if (!existsSync9(abs)) {
|
|
3379
3599
|
result.skipped.push(known.path);
|
|
3380
3600
|
continue;
|
|
3381
3601
|
}
|
|
3382
3602
|
try {
|
|
3383
|
-
const rawContent =
|
|
3603
|
+
const rawContent = readFileSync7(abs, "utf-8");
|
|
3384
3604
|
if (rawContent.length > 500000) {
|
|
3385
3605
|
result.skipped.push(known.path + " (too large)");
|
|
3386
3606
|
continue;
|
|
@@ -3444,6 +3664,10 @@ async function syncToDisk(opts = {}) {
|
|
|
3444
3664
|
for (const config of configs) {
|
|
3445
3665
|
if (!config.target_path && config.outputs.length === 0)
|
|
3446
3666
|
continue;
|
|
3667
|
+
if (isRetiredOrUnsupportedConfigAgent(config.agent)) {
|
|
3668
|
+
result.skipped.push(`${config.slug} (${retiredOrUnsupportedAgentReason(config.agent)})`);
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3447
3671
|
try {
|
|
3448
3672
|
const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
|
|
3449
3673
|
r.changed ? result.updated++ : result.unchanged++;
|
|
@@ -3455,9 +3679,9 @@ async function syncToDisk(opts = {}) {
|
|
|
3455
3679
|
}
|
|
3456
3680
|
function buildDiff(expectedContent, targetPath) {
|
|
3457
3681
|
const path = expandPath(targetPath);
|
|
3458
|
-
if (!
|
|
3682
|
+
if (!existsSync9(path))
|
|
3459
3683
|
return `(file not found on disk: ${path})`;
|
|
3460
|
-
const diskContent =
|
|
3684
|
+
const diskContent = readFileSync7(path, "utf-8");
|
|
3461
3685
|
if (diskContent === expectedContent)
|
|
3462
3686
|
return "(no diff \u2014 identical)";
|
|
3463
3687
|
const stored = expectedContent.split(`
|
|
@@ -3510,12 +3734,12 @@ async function diffConfig(config, opts = {}) {
|
|
|
3510
3734
|
}
|
|
3511
3735
|
function detectCategory(filePath) {
|
|
3512
3736
|
const p = filePath.toLowerCase().replace(getConfigHome(), "~");
|
|
3513
|
-
if (p.includes("/.claude/rules/") || p.includes("/.cursor/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("codewith.md") || p.endsWith("gemini.md") || p.endsWith(".mdc"))
|
|
3737
|
+
if (p.includes("/.claude/rules/") || p.includes("/.cursor/rules/") || p.includes("/.agents/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("codewith.md") || p.endsWith("aicopilot.md") || p.endsWith("/.gemini/gemini.md") || p.endsWith(".mdc"))
|
|
3514
3738
|
return "rules";
|
|
3515
|
-
if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/") || p.includes("/.config/opencode/") || p.includes("/.codewith/") || p.includes("/.config/aicopilot/"))
|
|
3516
|
-
return "agent";
|
|
3517
3739
|
if (p.includes(".mcp.json") || p.includes("mcp"))
|
|
3518
3740
|
return "mcp";
|
|
3741
|
+
if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.antigravity/") || p.includes("/.agents/") || p.includes("/.cursor/") || p.includes("/.config/opencode/") || p.includes("/.codewith/") || p.includes("/.config/aicopilot/"))
|
|
3742
|
+
return "agent";
|
|
3519
3743
|
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
|
|
3520
3744
|
return "shell";
|
|
3521
3745
|
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
@@ -3528,6 +3752,10 @@ function detectCategory(filePath) {
|
|
|
3528
3752
|
}
|
|
3529
3753
|
function detectAgent(filePath) {
|
|
3530
3754
|
const p = filePath.toLowerCase().replace(getConfigHome(), "~");
|
|
3755
|
+
if (p.endsWith("/.gemini/gemini.md") || p.endsWith("/.gemini/config/mcp_config.json"))
|
|
3756
|
+
return "antigravity";
|
|
3757
|
+
if (p.includes("/.agents/rules/") || p.endsWith("/.agents/mcp_config.json"))
|
|
3758
|
+
return "antigravity";
|
|
3531
3759
|
if (p.includes("/.claude/") || p.endsWith("claude.md"))
|
|
3532
3760
|
return "claude";
|
|
3533
3761
|
if (p.includes("/.config/opencode/"))
|
|
@@ -3538,10 +3766,10 @@ function detectAgent(filePath) {
|
|
|
3538
3766
|
return "codewith";
|
|
3539
3767
|
if (p.includes("/.config/aicopilot/"))
|
|
3540
3768
|
return "aicopilot";
|
|
3769
|
+
if (p.includes("/.antigravity/"))
|
|
3770
|
+
return "antigravity";
|
|
3541
3771
|
if (p.includes("/.codex/") || p.endsWith("agents.md"))
|
|
3542
3772
|
return "codex";
|
|
3543
|
-
if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
|
|
3544
|
-
return "gemini";
|
|
3545
3773
|
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
|
|
3546
3774
|
return "zsh";
|
|
3547
3775
|
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
@@ -3565,15 +3793,15 @@ function detectFormat(filePath) {
|
|
|
3565
3793
|
return "text";
|
|
3566
3794
|
}
|
|
3567
3795
|
// src/lib/export.ts
|
|
3568
|
-
import { existsSync as
|
|
3569
|
-
import { join as
|
|
3796
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3797
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
3570
3798
|
import { tmpdir } from "os";
|
|
3571
3799
|
async function exportConfigs(outputPath, opts = {}) {
|
|
3572
3800
|
const store = opts.store ?? resolveConfigStore();
|
|
3573
3801
|
const configs = await store.listConfigs(opts.filter);
|
|
3574
3802
|
const absOutput = resolve4(outputPath);
|
|
3575
|
-
const tmpDir =
|
|
3576
|
-
const contentsDir =
|
|
3803
|
+
const tmpDir = join8(tmpdir(), `configs-export-${Date.now()}`);
|
|
3804
|
+
const contentsDir = join8(tmpDir, "contents");
|
|
3577
3805
|
try {
|
|
3578
3806
|
mkdirSync4(contentsDir, { recursive: true });
|
|
3579
3807
|
const manifest = {
|
|
@@ -3581,10 +3809,10 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3581
3809
|
exported_at: new Date().toISOString(),
|
|
3582
3810
|
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
3583
3811
|
};
|
|
3584
|
-
writeFileSync3(
|
|
3812
|
+
writeFileSync3(join8(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
3585
3813
|
for (const config of configs) {
|
|
3586
3814
|
const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
|
|
3587
|
-
writeFileSync3(
|
|
3815
|
+
writeFileSync3(join8(contentsDir, fileName), config.content, "utf-8");
|
|
3588
3816
|
}
|
|
3589
3817
|
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
|
|
3590
3818
|
stdout: "pipe",
|
|
@@ -3597,20 +3825,20 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3597
3825
|
}
|
|
3598
3826
|
return { path: absOutput, count: configs.length };
|
|
3599
3827
|
} finally {
|
|
3600
|
-
if (
|
|
3828
|
+
if (existsSync10(tmpDir)) {
|
|
3601
3829
|
rmSync3(tmpDir, { recursive: true, force: true });
|
|
3602
3830
|
}
|
|
3603
3831
|
}
|
|
3604
3832
|
}
|
|
3605
3833
|
// src/lib/import.ts
|
|
3606
|
-
import { existsSync as
|
|
3607
|
-
import { join as
|
|
3834
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
|
|
3835
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
3608
3836
|
import { tmpdir as tmpdir2 } from "os";
|
|
3609
3837
|
async function importConfigs(bundlePath, opts = {}) {
|
|
3610
3838
|
const store = opts.store ?? resolveConfigStore();
|
|
3611
3839
|
const conflict = opts.conflict ?? "skip";
|
|
3612
3840
|
const absPath = resolve5(bundlePath);
|
|
3613
|
-
const tmpDir =
|
|
3841
|
+
const tmpDir = join9(tmpdir2(), `configs-import-${Date.now()}`);
|
|
3614
3842
|
const result = { created: 0, updated: 0, skipped: 0, errors: [] };
|
|
3615
3843
|
try {
|
|
3616
3844
|
mkdirSync5(tmpDir, { recursive: true });
|
|
@@ -3623,15 +3851,15 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3623
3851
|
const stderr = await new Response(proc.stderr).text();
|
|
3624
3852
|
throw new Error(`tar extraction failed: ${stderr}`);
|
|
3625
3853
|
}
|
|
3626
|
-
const manifestPath =
|
|
3627
|
-
if (!
|
|
3854
|
+
const manifestPath = join9(tmpDir, "manifest.json");
|
|
3855
|
+
if (!existsSync11(manifestPath))
|
|
3628
3856
|
throw new Error("Invalid bundle: missing manifest.json");
|
|
3629
|
-
const manifest = JSON.parse(
|
|
3857
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
|
|
3630
3858
|
for (const meta of manifest.configs) {
|
|
3631
3859
|
try {
|
|
3632
3860
|
const ext = meta.format === "text" ? "txt" : meta.format;
|
|
3633
|
-
const contentFile =
|
|
3634
|
-
const content =
|
|
3861
|
+
const contentFile = join9(tmpDir, "contents", `${meta.slug}.${ext}`);
|
|
3862
|
+
const content = existsSync11(contentFile) ? readFileSync8(contentFile, "utf-8") : "";
|
|
3635
3863
|
let existing = null;
|
|
3636
3864
|
try {
|
|
3637
3865
|
existing = await store.getConfig(meta.slug);
|
|
@@ -3665,16 +3893,16 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3665
3893
|
}
|
|
3666
3894
|
return result;
|
|
3667
3895
|
} finally {
|
|
3668
|
-
if (
|
|
3896
|
+
if (existsSync11(tmpDir)) {
|
|
3669
3897
|
rmSync4(tmpDir, { recursive: true, force: true });
|
|
3670
3898
|
}
|
|
3671
3899
|
}
|
|
3672
3900
|
}
|
|
3673
3901
|
// src/lib/package-manager-guard.ts
|
|
3674
3902
|
import { execFileSync } from "child_process";
|
|
3675
|
-
import { existsSync as
|
|
3903
|
+
import { existsSync as existsSync12, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
3676
3904
|
import { homedir as homedir5 } from "os";
|
|
3677
|
-
import { basename as basename5, dirname as
|
|
3905
|
+
import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve6 } from "path";
|
|
3678
3906
|
var SKIP_DIRS = new Set([
|
|
3679
3907
|
".git",
|
|
3680
3908
|
"node_modules",
|
|
@@ -3716,7 +3944,7 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
3716
3944
|
const findings = [];
|
|
3717
3945
|
let scannedFiles = 0;
|
|
3718
3946
|
for (const root of roots) {
|
|
3719
|
-
if (!
|
|
3947
|
+
if (!existsSync12(root))
|
|
3720
3948
|
continue;
|
|
3721
3949
|
const stat = lstatSync2(root);
|
|
3722
3950
|
if (stat.isFile()) {
|
|
@@ -3726,7 +3954,7 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
3726
3954
|
if (text === null)
|
|
3727
3955
|
continue;
|
|
3728
3956
|
scannedFiles++;
|
|
3729
|
-
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root),
|
|
3957
|
+
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname5(root)));
|
|
3730
3958
|
continue;
|
|
3731
3959
|
}
|
|
3732
3960
|
if (!stat.isDirectory())
|
|
@@ -3745,8 +3973,8 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
3745
3973
|
if (options.includeHome) {
|
|
3746
3974
|
const home = homedir5();
|
|
3747
3975
|
for (const name of HOME_FILES) {
|
|
3748
|
-
const file =
|
|
3749
|
-
if (!
|
|
3976
|
+
const file = join10(home, name);
|
|
3977
|
+
if (!existsSync12(file))
|
|
3750
3978
|
continue;
|
|
3751
3979
|
const text = readTextFile(file);
|
|
3752
3980
|
if (text === null)
|
|
@@ -3770,12 +3998,12 @@ function collectRepoFiles(root) {
|
|
|
3770
3998
|
if (entry.isDirectory()) {
|
|
3771
3999
|
if (SKIP_DIRS.has(entry.name))
|
|
3772
4000
|
continue;
|
|
3773
|
-
visit(
|
|
4001
|
+
visit(join10(dir, entry.name));
|
|
3774
4002
|
continue;
|
|
3775
4003
|
}
|
|
3776
4004
|
if (!entry.isFile())
|
|
3777
4005
|
continue;
|
|
3778
|
-
const file =
|
|
4006
|
+
const file = join10(dir, entry.name);
|
|
3779
4007
|
if (shouldScanRepoFile(file))
|
|
3780
4008
|
out.push(file);
|
|
3781
4009
|
}
|
|
@@ -3813,7 +4041,7 @@ function readTextFile(file) {
|
|
|
3813
4041
|
const stat = lstatSync2(file);
|
|
3814
4042
|
if (!stat.isFile() || stat.size > 5000000)
|
|
3815
4043
|
return null;
|
|
3816
|
-
const buf =
|
|
4044
|
+
const buf = readFileSync9(file);
|
|
3817
4045
|
if (buf.includes(0))
|
|
3818
4046
|
return null;
|
|
3819
4047
|
return buf.toString("utf-8");
|
|
@@ -4013,7 +4241,7 @@ function trackedFiles(root) {
|
|
|
4013
4241
|
}
|
|
4014
4242
|
function isTrackedFile(file) {
|
|
4015
4243
|
try {
|
|
4016
|
-
const repoRoot = execFileSync("git", ["-C",
|
|
4244
|
+
const repoRoot = execFileSync("git", ["-C", dirname5(file), "rev-parse", "--show-toplevel"], {
|
|
4017
4245
|
encoding: "utf-8",
|
|
4018
4246
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4019
4247
|
}).trim();
|
|
@@ -4094,6 +4322,7 @@ export {
|
|
|
4094
4322
|
expandPath,
|
|
4095
4323
|
ensureProjectDashboardStandardConfig,
|
|
4096
4324
|
ensurePlatformProfiles,
|
|
4325
|
+
ensureGlobalAgentRulesStandardConfig,
|
|
4097
4326
|
diffConfig,
|
|
4098
4327
|
detectMachineContext,
|
|
4099
4328
|
detectFormat,
|
|
@@ -4117,6 +4346,8 @@ export {
|
|
|
4117
4346
|
SESSION_RENDER_TOOLS,
|
|
4118
4347
|
SESSION_RENDER_SCHEMA,
|
|
4119
4348
|
SESSION_RENDER_MANAGED_MARKER,
|
|
4349
|
+
SESSION_LAYER_RANK,
|
|
4350
|
+
SESSION_INSTRUCTION_LAYERS,
|
|
4120
4351
|
RAW_STORE_ROOT_ENV,
|
|
4121
4352
|
ProfileNotFoundError,
|
|
4122
4353
|
PROJECT_DASHBOARD_STANDARD_SLUG,
|
|
@@ -4127,6 +4358,8 @@ export {
|
|
|
4127
4358
|
PG_MIGRATIONS,
|
|
4128
4359
|
LocalConfigStore,
|
|
4129
4360
|
KNOWN_CONFIGS,
|
|
4361
|
+
GLOBAL_AGENT_RULES_STANDARD_SLUG,
|
|
4362
|
+
GLOBAL_AGENT_RULES_STANDARD_CONTENT,
|
|
4130
4363
|
ConfigNotFoundError,
|
|
4131
4364
|
ConfigApplyError,
|
|
4132
4365
|
CloudHttpError,
|