@hasna/instructions 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +180 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8080 -0
- package/dist/cli/output.test.d.ts +2 -0
- package/dist/cli/output.test.d.ts.map +1 -0
- package/dist/cli/session.test.d.ts +2 -0
- package/dist/cli/session.test.d.ts.map +1 -0
- package/dist/cli/storage.d.ts +3 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/cli/storage.test.d.ts +2 -0
- package/dist/cli/storage.test.d.ts.map +1 -0
- package/dist/db/configs.d.ts +10 -0
- package/dist/db/configs.d.ts.map +1 -0
- package/dist/db/configs.test.d.ts +2 -0
- package/dist/db/configs.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +7 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.test.d.ts +2 -0
- package/dist/db/database.test.d.ts.map +1 -0
- package/dist/db/machines.d.ts +9 -0
- package/dist/db/machines.d.ts.map +1 -0
- package/dist/db/machines.test.d.ts +2 -0
- package/dist/db/machines.test.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts +7 -0
- package/dist/db/pg-migrations.d.ts.map +1 -0
- package/dist/db/profiles.d.ts +14 -0
- package/dist/db/profiles.d.ts.map +1 -0
- package/dist/db/profiles.test.d.ts +2 -0
- package/dist/db/profiles.test.d.ts.map +1 -0
- package/dist/db/remote-storage.d.ts +13 -0
- package/dist/db/remote-storage.d.ts.map +1 -0
- package/dist/db/snapshots.d.ts +8 -0
- package/dist/db/snapshots.d.ts.map +1 -0
- package/dist/db/snapshots.test.d.ts +2 -0
- package/dist/db/snapshots.test.d.ts.map +1 -0
- package/dist/db/storage-sync.d.ts +53 -0
- package/dist/db/storage-sync.d.ts.map +1 -0
- package/dist/db/storage-sync.test.d.ts +2 -0
- package/dist/db/storage-sync.test.d.ts.map +1 -0
- package/dist/generated/storage-kit/health.d.ts +20 -0
- package/dist/generated/storage-kit/health.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +8 -0
- package/dist/generated/storage-kit/index.d.ts.map +1 -0
- package/dist/generated/storage-kit/migrations.d.ts +48 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -0
- package/dist/generated/storage-kit/mode.d.ts +48 -0
- package/dist/generated/storage-kit/mode.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +34 -0
- package/dist/generated/storage-kit/pool.d.ts.map +1 -0
- package/dist/generated/storage-kit/query.d.ts +36 -0
- package/dist/generated/storage-kit/query.d.ts.map +1 -0
- package/dist/generated/storage-kit/tls.d.ts +26 -0
- package/dist/generated/storage-kit/tls.d.ts.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3749 -0
- package/dist/lib/apply-batch.test.d.ts +2 -0
- package/dist/lib/apply-batch.test.d.ts.map +1 -0
- package/dist/lib/apply.d.ts +16 -0
- package/dist/lib/apply.d.ts.map +1 -0
- package/dist/lib/apply.test.d.ts +2 -0
- package/dist/lib/apply.test.d.ts.map +1 -0
- package/dist/lib/compact-output.d.ts +78 -0
- package/dist/lib/compact-output.d.ts.map +1 -0
- package/dist/lib/compact-output.test.d.ts +2 -0
- package/dist/lib/compact-output.test.d.ts.map +1 -0
- package/dist/lib/export-import.test.d.ts +2 -0
- package/dist/lib/export-import.test.d.ts.map +1 -0
- package/dist/lib/export.d.ts +12 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/import.d.ts +14 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +21 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.test.d.ts +2 -0
- package/dist/lib/machine.test.d.ts.map +1 -0
- package/dist/lib/package-version.d.ts +2 -0
- package/dist/lib/package-version.d.ts.map +1 -0
- package/dist/lib/platform-profiles.d.ts +5 -0
- package/dist/lib/platform-profiles.d.ts.map +1 -0
- package/dist/lib/project-dashboard-standard.d.ts +7 -0
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -0
- package/dist/lib/project-dashboard-standard.test.d.ts +2 -0
- package/dist/lib/project-dashboard-standard.test.d.ts.map +1 -0
- package/dist/lib/redact.d.ts +29 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.test.d.ts +2 -0
- package/dist/lib/redact.test.d.ts.map +1 -0
- package/dist/lib/session-apply.d.ts +48 -0
- package/dist/lib/session-apply.d.ts.map +1 -0
- package/dist/lib/session-apply.test.d.ts +2 -0
- package/dist/lib/session-apply.test.d.ts.map +1 -0
- package/dist/lib/session-render.d.ts +176 -0
- package/dist/lib/session-render.d.ts.map +1 -0
- package/dist/lib/session-render.test.d.ts +2 -0
- package/dist/lib/session-render.test.d.ts.map +1 -0
- package/dist/lib/sync-dir.d.ts +13 -0
- package/dist/lib/sync-dir.d.ts.map +1 -0
- package/dist/lib/sync-dir.test.d.ts +2 -0
- package/dist/lib/sync-dir.test.d.ts.map +1 -0
- package/dist/lib/sync-known.test.d.ts +2 -0
- package/dist/lib/sync-known.test.d.ts.map +1 -0
- package/dist/lib/sync.d.ts +54 -0
- package/dist/lib/sync.d.ts.map +1 -0
- package/dist/lib/sync.test.d.ts +2 -0
- package/dist/lib/sync.test.d.ts.map +1 -0
- package/dist/lib/template.d.ts +10 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.test.d.ts +2 -0
- package/dist/lib/template.test.d.ts.map +1 -0
- package/dist/lib/transforms.d.ts +11 -0
- package/dist/lib/transforms.d.ts.map +1 -0
- package/dist/lib/transforms.test.d.ts +2 -0
- package/dist/lib/transforms.test.d.ts.map +1 -0
- package/dist/mcp/http.d.ts +28 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.test.d.ts +2 -0
- package/dist/mcp/http.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2591 -0
- package/dist/mcp/mcp.test.d.ts +2 -0
- package/dist/mcp/mcp.test.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +39 -0
- package/dist/server/cloud.d.ts.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +19062 -0
- package/dist/server/openapi.d.ts +578 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/server.test.d.ts +2 -0
- package/dist/server/server.test.d.ts.map +1 -0
- package/dist/server/v1.d.ts +9 -0
- package/dist/server/v1.d.ts.map +1 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.test.d.ts +2 -0
- package/dist/status.test.d.ts.map +1 -0
- package/dist/storage/cloud-store.d.ts +17 -0
- package/dist/storage/cloud-store.d.ts.map +1 -0
- package/dist/storage/schema.d.ts +12 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +537 -0
- package/dist/types/index.d.ts +189 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,2591 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
5
|
+
var __returnValue = (v) => v;
|
|
6
|
+
function __exportSetter(name, newValue) {
|
|
7
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
8
|
+
}
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
set: __exportSetter.bind(all, name)
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
var __require = import.meta.require;
|
|
20
|
+
|
|
21
|
+
// src/db/database.ts
|
|
22
|
+
var exports_database = {};
|
|
23
|
+
__export(exports_database, {
|
|
24
|
+
uuid: () => uuid,
|
|
25
|
+
slugify: () => slugify,
|
|
26
|
+
resetDatabase: () => resetDatabase,
|
|
27
|
+
now: () => now,
|
|
28
|
+
getDatabase: () => getDatabase
|
|
29
|
+
});
|
|
30
|
+
import { Database } from "bun:sqlite";
|
|
31
|
+
import { cpSync, existsSync, mkdirSync, statSync } from "fs";
|
|
32
|
+
import { join } from "path";
|
|
33
|
+
import { randomUUID } from "crypto";
|
|
34
|
+
function getDbPath() {
|
|
35
|
+
if (process.env["HASNA_CONFIGS_DB_PATH"]) {
|
|
36
|
+
return process.env["HASNA_CONFIGS_DB_PATH"];
|
|
37
|
+
}
|
|
38
|
+
if (process.env["CONFIGS_DB_PATH"]) {
|
|
39
|
+
return process.env["CONFIGS_DB_PATH"];
|
|
40
|
+
}
|
|
41
|
+
migrateDotfile();
|
|
42
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
43
|
+
const dir = join(home, ".hasna", "configs");
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
return join(dir, "configs.db");
|
|
46
|
+
}
|
|
47
|
+
function uuid() {
|
|
48
|
+
return randomUUID();
|
|
49
|
+
}
|
|
50
|
+
function now() {
|
|
51
|
+
return new Date().toISOString();
|
|
52
|
+
}
|
|
53
|
+
function slugify(name) {
|
|
54
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
55
|
+
}
|
|
56
|
+
function getDatabase(path) {
|
|
57
|
+
if (_db)
|
|
58
|
+
return _db;
|
|
59
|
+
const dbPath = path || getDbPath();
|
|
60
|
+
const db = new Database(dbPath);
|
|
61
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
62
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
63
|
+
applyMigrations(db);
|
|
64
|
+
ensureFeedbackTable(db);
|
|
65
|
+
_db = db;
|
|
66
|
+
return db;
|
|
67
|
+
}
|
|
68
|
+
function resetDatabase() {
|
|
69
|
+
if (_db) {
|
|
70
|
+
try {
|
|
71
|
+
_db.close();
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
_db = null;
|
|
75
|
+
}
|
|
76
|
+
function applyMigrations(db) {
|
|
77
|
+
let currentVersion = 0;
|
|
78
|
+
try {
|
|
79
|
+
const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
|
|
80
|
+
currentVersion = row?.version ?? 0;
|
|
81
|
+
} catch {
|
|
82
|
+
currentVersion = 0;
|
|
83
|
+
}
|
|
84
|
+
for (let i = currentVersion;i < MIGRATIONS.length; i++) {
|
|
85
|
+
db.exec(MIGRATIONS[i]);
|
|
86
|
+
db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function ensureFeedbackTable(db) {
|
|
90
|
+
db.exec(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
92
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
93
|
+
message TEXT NOT NULL,
|
|
94
|
+
email TEXT,
|
|
95
|
+
category TEXT DEFAULT 'general',
|
|
96
|
+
version TEXT,
|
|
97
|
+
machine_id TEXT,
|
|
98
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
99
|
+
)
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
function migrateDotfile() {
|
|
103
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
104
|
+
const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
|
|
105
|
+
const newDir = join(home, ".hasna", "configs");
|
|
106
|
+
if (existsSync(newDir))
|
|
107
|
+
return;
|
|
108
|
+
for (const oldDir of oldDirs) {
|
|
109
|
+
if (!existsSync(oldDir))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
if (!statSync(oldDir).isDirectory())
|
|
113
|
+
continue;
|
|
114
|
+
mkdirSync(join(home, ".hasna"), { recursive: true });
|
|
115
|
+
cpSync(oldDir, newDir, { recursive: true, force: false });
|
|
116
|
+
return;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var MIGRATIONS, _db = null;
|
|
121
|
+
var init_database = __esm(() => {
|
|
122
|
+
MIGRATIONS = [
|
|
123
|
+
`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS configs (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
name TEXT NOT NULL,
|
|
127
|
+
slug TEXT NOT NULL UNIQUE,
|
|
128
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
129
|
+
category TEXT NOT NULL,
|
|
130
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
131
|
+
target_path TEXT,
|
|
132
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
133
|
+
content TEXT NOT NULL DEFAULT '',
|
|
134
|
+
description TEXT,
|
|
135
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
is_template INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
138
|
+
created_at TEXT NOT NULL,
|
|
139
|
+
updated_at TEXT NOT NULL,
|
|
140
|
+
synced_at TEXT
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
144
|
+
id TEXT PRIMARY KEY,
|
|
145
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
146
|
+
content TEXT NOT NULL,
|
|
147
|
+
version INTEGER NOT NULL,
|
|
148
|
+
created_at TEXT NOT NULL
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
152
|
+
id TEXT PRIMARY KEY,
|
|
153
|
+
name TEXT NOT NULL,
|
|
154
|
+
slug TEXT NOT NULL UNIQUE,
|
|
155
|
+
description TEXT,
|
|
156
|
+
created_at TEXT NOT NULL,
|
|
157
|
+
updated_at TEXT NOT NULL
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE IF NOT EXISTS profile_configs (
|
|
161
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
162
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
163
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
PRIMARY KEY (profile_id, config_id)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
170
|
+
os TEXT,
|
|
171
|
+
last_applied_at TEXT,
|
|
172
|
+
created_at TEXT NOT NULL
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
176
|
+
version INTEGER PRIMARY KEY
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
|
|
180
|
+
`,
|
|
181
|
+
`
|
|
182
|
+
ALTER TABLE profiles ADD COLUMN selectors TEXT NOT NULL DEFAULT '{}';
|
|
183
|
+
ALTER TABLE profiles ADD COLUMN variables TEXT NOT NULL DEFAULT '{}';
|
|
184
|
+
ALTER TABLE machines ADD COLUMN arch TEXT;
|
|
185
|
+
`,
|
|
186
|
+
`
|
|
187
|
+
ALTER TABLE configs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]';
|
|
188
|
+
`
|
|
189
|
+
];
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// src/types/index.ts
|
|
193
|
+
var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
|
|
194
|
+
var init_types = __esm(() => {
|
|
195
|
+
ConfigNotFoundError = class ConfigNotFoundError extends Error {
|
|
196
|
+
constructor(id) {
|
|
197
|
+
super(`Config not found: ${id}`);
|
|
198
|
+
this.name = "ConfigNotFoundError";
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
ProfileNotFoundError = class ProfileNotFoundError extends Error {
|
|
202
|
+
constructor(id) {
|
|
203
|
+
super(`Profile not found: ${id}`);
|
|
204
|
+
this.name = "ProfileNotFoundError";
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
ConfigApplyError = class ConfigApplyError extends Error {
|
|
208
|
+
constructor(message) {
|
|
209
|
+
super(message);
|
|
210
|
+
this.name = "ConfigApplyError";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
TemplateRenderError = class TemplateRenderError extends Error {
|
|
214
|
+
constructor(message) {
|
|
215
|
+
super(message);
|
|
216
|
+
this.name = "TemplateRenderError";
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/db/configs.ts
|
|
222
|
+
var exports_configs = {};
|
|
223
|
+
__export(exports_configs, {
|
|
224
|
+
updateConfig: () => updateConfig,
|
|
225
|
+
listConfigs: () => listConfigs,
|
|
226
|
+
getConfigStats: () => getConfigStats,
|
|
227
|
+
getConfigById: () => getConfigById,
|
|
228
|
+
getConfig: () => getConfig,
|
|
229
|
+
deleteConfig: () => deleteConfig,
|
|
230
|
+
createConfig: () => createConfig
|
|
231
|
+
});
|
|
232
|
+
function rowToConfig(row) {
|
|
233
|
+
let outputs = [];
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(row.outputs || "[]");
|
|
236
|
+
outputs = Array.isArray(parsed) ? parsed : [];
|
|
237
|
+
} catch {
|
|
238
|
+
outputs = [];
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...row,
|
|
242
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
243
|
+
outputs,
|
|
244
|
+
is_template: !!row.is_template,
|
|
245
|
+
kind: row.kind,
|
|
246
|
+
category: row.category,
|
|
247
|
+
agent: row.agent,
|
|
248
|
+
format: row.format
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function uniqueSlug(name, db, excludeId) {
|
|
252
|
+
const base = slugify(name);
|
|
253
|
+
let slug = base;
|
|
254
|
+
let i = 1;
|
|
255
|
+
while (true) {
|
|
256
|
+
const existing = db.query("SELECT id FROM configs WHERE slug = ?").get(slug);
|
|
257
|
+
if (!existing || existing.id === excludeId)
|
|
258
|
+
return slug;
|
|
259
|
+
slug = `${base}-${i++}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function createConfig(input, db) {
|
|
263
|
+
const d = db || getDatabase();
|
|
264
|
+
const id = uuid();
|
|
265
|
+
const ts = now();
|
|
266
|
+
const slug = uniqueSlug(input.name, d);
|
|
267
|
+
const tags = JSON.stringify(input.tags || []);
|
|
268
|
+
const outputs = JSON.stringify(input.outputs || []);
|
|
269
|
+
d.run(`INSERT INTO configs (id, name, slug, kind, category, agent, target_path, outputs, format, content, description, tags, is_template, version, created_at, updated_at, synced_at)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, NULL)`, [
|
|
271
|
+
id,
|
|
272
|
+
input.name,
|
|
273
|
+
slug,
|
|
274
|
+
input.kind ?? "file",
|
|
275
|
+
input.category,
|
|
276
|
+
input.agent ?? "global",
|
|
277
|
+
input.target_path ?? null,
|
|
278
|
+
outputs,
|
|
279
|
+
input.format ?? "text",
|
|
280
|
+
input.content,
|
|
281
|
+
input.description ?? null,
|
|
282
|
+
tags,
|
|
283
|
+
input.is_template ? 1 : 0,
|
|
284
|
+
ts,
|
|
285
|
+
ts
|
|
286
|
+
]);
|
|
287
|
+
return getConfig(id, d);
|
|
288
|
+
}
|
|
289
|
+
function getConfig(idOrSlug, db) {
|
|
290
|
+
const d = db || getDatabase();
|
|
291
|
+
const row = d.query("SELECT * FROM configs WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
292
|
+
if (!row)
|
|
293
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
294
|
+
return rowToConfig(row);
|
|
295
|
+
}
|
|
296
|
+
function getConfigById(id, db) {
|
|
297
|
+
const d = db || getDatabase();
|
|
298
|
+
const row = d.query("SELECT * FROM configs WHERE id = ?").get(id);
|
|
299
|
+
if (!row)
|
|
300
|
+
throw new ConfigNotFoundError(id);
|
|
301
|
+
return rowToConfig(row);
|
|
302
|
+
}
|
|
303
|
+
function listConfigs(filter, db) {
|
|
304
|
+
const d = db || getDatabase();
|
|
305
|
+
const conditions = [];
|
|
306
|
+
const params = [];
|
|
307
|
+
if (filter?.category) {
|
|
308
|
+
conditions.push("category = ?");
|
|
309
|
+
params.push(filter.category);
|
|
310
|
+
}
|
|
311
|
+
if (filter?.agent) {
|
|
312
|
+
conditions.push("agent = ?");
|
|
313
|
+
params.push(filter.agent);
|
|
314
|
+
}
|
|
315
|
+
if (filter?.kind) {
|
|
316
|
+
conditions.push("kind = ?");
|
|
317
|
+
params.push(filter.kind);
|
|
318
|
+
}
|
|
319
|
+
if (filter?.is_template !== undefined) {
|
|
320
|
+
conditions.push("is_template = ?");
|
|
321
|
+
params.push(filter.is_template ? 1 : 0);
|
|
322
|
+
}
|
|
323
|
+
if (filter?.search) {
|
|
324
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
325
|
+
const q = `%${filter.search}%`;
|
|
326
|
+
params.push(q, q, q);
|
|
327
|
+
}
|
|
328
|
+
if (filter?.tags && filter.tags.length > 0) {
|
|
329
|
+
const tagConditions = filter.tags.map(() => "tags LIKE ?").join(" OR ");
|
|
330
|
+
conditions.push(`(${tagConditions})`);
|
|
331
|
+
for (const tag of filter.tags)
|
|
332
|
+
params.push(`%"${tag}"%`);
|
|
333
|
+
}
|
|
334
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
335
|
+
const rows = d.query(`SELECT * FROM configs ${where} ORDER BY category, name`).all(...params);
|
|
336
|
+
return rows.map(rowToConfig);
|
|
337
|
+
}
|
|
338
|
+
function updateConfig(idOrSlug, input, db) {
|
|
339
|
+
const d = db || getDatabase();
|
|
340
|
+
const existing = getConfig(idOrSlug, d);
|
|
341
|
+
const ts = now();
|
|
342
|
+
const updates = ["updated_at = ?", "version = version + 1"];
|
|
343
|
+
const params = [ts];
|
|
344
|
+
if (input.name !== undefined) {
|
|
345
|
+
updates.push("name = ?", "slug = ?");
|
|
346
|
+
params.push(input.name, uniqueSlug(input.name, d, existing.id));
|
|
347
|
+
}
|
|
348
|
+
if (input.kind !== undefined) {
|
|
349
|
+
updates.push("kind = ?");
|
|
350
|
+
params.push(input.kind);
|
|
351
|
+
}
|
|
352
|
+
if (input.category !== undefined) {
|
|
353
|
+
updates.push("category = ?");
|
|
354
|
+
params.push(input.category);
|
|
355
|
+
}
|
|
356
|
+
if (input.agent !== undefined) {
|
|
357
|
+
updates.push("agent = ?");
|
|
358
|
+
params.push(input.agent);
|
|
359
|
+
}
|
|
360
|
+
if (input.target_path !== undefined) {
|
|
361
|
+
updates.push("target_path = ?");
|
|
362
|
+
params.push(input.target_path);
|
|
363
|
+
}
|
|
364
|
+
if (input.outputs !== undefined) {
|
|
365
|
+
updates.push("outputs = ?");
|
|
366
|
+
params.push(JSON.stringify(input.outputs));
|
|
367
|
+
}
|
|
368
|
+
if (input.format !== undefined) {
|
|
369
|
+
updates.push("format = ?");
|
|
370
|
+
params.push(input.format);
|
|
371
|
+
}
|
|
372
|
+
if (input.content !== undefined) {
|
|
373
|
+
updates.push("content = ?");
|
|
374
|
+
params.push(input.content);
|
|
375
|
+
}
|
|
376
|
+
if (input.description !== undefined) {
|
|
377
|
+
updates.push("description = ?");
|
|
378
|
+
params.push(input.description);
|
|
379
|
+
}
|
|
380
|
+
if (input.tags !== undefined) {
|
|
381
|
+
updates.push("tags = ?");
|
|
382
|
+
params.push(JSON.stringify(input.tags));
|
|
383
|
+
}
|
|
384
|
+
if (input.is_template !== undefined) {
|
|
385
|
+
updates.push("is_template = ?");
|
|
386
|
+
params.push(input.is_template ? 1 : 0);
|
|
387
|
+
}
|
|
388
|
+
if (input.synced_at !== undefined) {
|
|
389
|
+
updates.push("synced_at = ?");
|
|
390
|
+
params.push(input.synced_at);
|
|
391
|
+
}
|
|
392
|
+
params.push(existing.id);
|
|
393
|
+
d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
394
|
+
return getConfigById(existing.id, d);
|
|
395
|
+
}
|
|
396
|
+
function deleteConfig(idOrSlug, db) {
|
|
397
|
+
const d = db || getDatabase();
|
|
398
|
+
const existing = getConfig(idOrSlug, d);
|
|
399
|
+
d.run("DELETE FROM configs WHERE id = ?", [existing.id]);
|
|
400
|
+
}
|
|
401
|
+
function getConfigStats(db) {
|
|
402
|
+
const d = db || getDatabase();
|
|
403
|
+
const rows = d.query("SELECT category, COUNT(*) as count FROM configs GROUP BY category").all();
|
|
404
|
+
const stats = { total: 0 };
|
|
405
|
+
for (const row of rows) {
|
|
406
|
+
stats[row.category] = row.count;
|
|
407
|
+
stats["total"] = (stats["total"] || 0) + row.count;
|
|
408
|
+
}
|
|
409
|
+
return stats;
|
|
410
|
+
}
|
|
411
|
+
var init_configs = __esm(() => {
|
|
412
|
+
init_types();
|
|
413
|
+
init_database();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// src/db/snapshots.ts
|
|
417
|
+
function createSnapshot(configId, content, version, db) {
|
|
418
|
+
const d = db || getDatabase();
|
|
419
|
+
const id = uuid();
|
|
420
|
+
const ts = now();
|
|
421
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
422
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
423
|
+
}
|
|
424
|
+
function listSnapshots(configId, db) {
|
|
425
|
+
const d = db || getDatabase();
|
|
426
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
427
|
+
}
|
|
428
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
429
|
+
const d = db || getDatabase();
|
|
430
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
431
|
+
}
|
|
432
|
+
var init_snapshots = __esm(() => {
|
|
433
|
+
init_database();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// src/lib/template.ts
|
|
437
|
+
var exports_template = {};
|
|
438
|
+
__export(exports_template, {
|
|
439
|
+
renderTemplate: () => renderTemplate,
|
|
440
|
+
parseTemplateVars: () => parseTemplateVars,
|
|
441
|
+
isTemplate: () => isTemplate,
|
|
442
|
+
extractTemplateVars: () => extractTemplateVars
|
|
443
|
+
});
|
|
444
|
+
function parseTemplateVars(content) {
|
|
445
|
+
const names = new Set;
|
|
446
|
+
let match;
|
|
447
|
+
VAR_PATTERN.lastIndex = 0;
|
|
448
|
+
while ((match = VAR_PATTERN.exec(content)) !== null) {
|
|
449
|
+
names.add(match[1]);
|
|
450
|
+
}
|
|
451
|
+
return Array.from(names);
|
|
452
|
+
}
|
|
453
|
+
function extractTemplateVars(content) {
|
|
454
|
+
const vars = new Map;
|
|
455
|
+
let match;
|
|
456
|
+
VAR_PATTERN.lastIndex = 0;
|
|
457
|
+
while ((match = VAR_PATTERN.exec(content)) !== null) {
|
|
458
|
+
const name = match[1];
|
|
459
|
+
const description = match[2] ?? null;
|
|
460
|
+
if (!vars.has(name)) {
|
|
461
|
+
vars.set(name, { name, description, required: true });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return Array.from(vars.values());
|
|
465
|
+
}
|
|
466
|
+
function renderTemplate(content, vars) {
|
|
467
|
+
const missing = [];
|
|
468
|
+
VAR_PATTERN.lastIndex = 0;
|
|
469
|
+
let match;
|
|
470
|
+
while ((match = VAR_PATTERN.exec(content)) !== null) {
|
|
471
|
+
const name = match[1];
|
|
472
|
+
if (!(name in vars))
|
|
473
|
+
missing.push(name);
|
|
474
|
+
}
|
|
475
|
+
if (missing.length > 0) {
|
|
476
|
+
throw new TemplateRenderError(`Missing required template variables: ${missing.join(", ")}`);
|
|
477
|
+
}
|
|
478
|
+
VAR_PATTERN.lastIndex = 0;
|
|
479
|
+
return content.replace(VAR_PATTERN, (_match, name) => vars[name] ?? "");
|
|
480
|
+
}
|
|
481
|
+
function isTemplate(content) {
|
|
482
|
+
VAR_PATTERN.lastIndex = 0;
|
|
483
|
+
return VAR_PATTERN.test(content);
|
|
484
|
+
}
|
|
485
|
+
var VAR_PATTERN;
|
|
486
|
+
var init_template = __esm(() => {
|
|
487
|
+
init_types();
|
|
488
|
+
VAR_PATTERN = /\{\{([A-Z0-9_]+)(?::([^}]*))?\}\}/g;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// src/lib/machine.ts
|
|
492
|
+
import { arch as currentArch, homedir, hostname as currentHostname, type as currentOsType } from "os";
|
|
493
|
+
import { existsSync as existsSync2 } from "fs";
|
|
494
|
+
import { join as join2 } from "path";
|
|
495
|
+
function normalizeOsFamily(os) {
|
|
496
|
+
const value = (os ?? "").trim().toLowerCase();
|
|
497
|
+
if (value === "darwin" || value === "macos" || value === "mac" || value === "osx")
|
|
498
|
+
return "macos";
|
|
499
|
+
if (value === "linux")
|
|
500
|
+
return "linux";
|
|
501
|
+
if (value === "windows_nt" || value === "windows" || value === "win32")
|
|
502
|
+
return "windows";
|
|
503
|
+
return value || "unknown";
|
|
504
|
+
}
|
|
505
|
+
function detectMachineContext(overrides = {}) {
|
|
506
|
+
const homeDir = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir();
|
|
507
|
+
const os = overrides.os ?? currentOsType();
|
|
508
|
+
const osFamily = normalizeOsFamily(os);
|
|
509
|
+
const bunBinDir = overrides.bun_bin_dir ?? join2(homeDir, ".bun", "bin");
|
|
510
|
+
const defaultBunPath = osFamily === "macos" && existsSync2(BREW_BUN_PATH) ? BREW_BUN_PATH : join2(bunBinDir, "bun");
|
|
511
|
+
return {
|
|
512
|
+
id: "current-machine",
|
|
513
|
+
hostname: overrides.hostname ?? currentHostname(),
|
|
514
|
+
os,
|
|
515
|
+
arch: overrides.arch ?? currentArch(),
|
|
516
|
+
last_applied_at: null,
|
|
517
|
+
created_at: "",
|
|
518
|
+
os_family: osFamily,
|
|
519
|
+
home_dir: homeDir,
|
|
520
|
+
workspace_root: overrides.workspace_root ?? join2(homeDir, osFamily === "macos" ? "Workspace" : "workspace"),
|
|
521
|
+
bun_bin_dir: bunBinDir,
|
|
522
|
+
bun_path: overrides.bun_path ?? defaultBunPath,
|
|
523
|
+
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join2("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function machineContextToVariables(machine) {
|
|
527
|
+
return {
|
|
528
|
+
HOSTNAME: machine.hostname,
|
|
529
|
+
OS: machine.os ?? "",
|
|
530
|
+
OS_FAMILY: machine.os_family,
|
|
531
|
+
ARCH: machine.arch ?? "",
|
|
532
|
+
HOME_DIR: machine.home_dir,
|
|
533
|
+
WORKSPACE_ROOT: machine.workspace_root,
|
|
534
|
+
BUN_BIN_DIR: machine.bun_bin_dir,
|
|
535
|
+
BUN_PATH: machine.bun_path,
|
|
536
|
+
PATH_PREFIX: machine.path_prefix
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function resolveProfileVariables(profile, machine) {
|
|
540
|
+
const base = machineContextToVariables(machine);
|
|
541
|
+
const raw = profile?.variables ?? {};
|
|
542
|
+
const resolved = { ...base };
|
|
543
|
+
const pending = new Map(Object.entries(raw));
|
|
544
|
+
const maxPasses = Math.max(4, pending.size * 2);
|
|
545
|
+
for (let pass = 0;pass < maxPasses && pending.size > 0; pass++) {
|
|
546
|
+
let progress = false;
|
|
547
|
+
for (const [key, value] of [...pending.entries()]) {
|
|
548
|
+
try {
|
|
549
|
+
resolved[key] = isTemplate(value) ? renderTemplate(value, resolved) : value;
|
|
550
|
+
pending.delete(key);
|
|
551
|
+
progress = true;
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
if (!progress)
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
for (const [key, value] of pending.entries()) {
|
|
558
|
+
resolved[key] = value;
|
|
559
|
+
}
|
|
560
|
+
return resolved;
|
|
561
|
+
}
|
|
562
|
+
function replaceKnownPath(content, actual, placeholder) {
|
|
563
|
+
if (!actual || actual === placeholder)
|
|
564
|
+
return content;
|
|
565
|
+
return content.split(actual).join(placeholder);
|
|
566
|
+
}
|
|
567
|
+
function templateizeMachineContent(content, machine) {
|
|
568
|
+
const vars = machineContextToVariables(machine);
|
|
569
|
+
let next = content;
|
|
570
|
+
const replacements = [
|
|
571
|
+
[vars["WORKSPACE_ROOT"], "{{WORKSPACE_ROOT}}"],
|
|
572
|
+
[vars["BUN_PATH"], "{{BUN_PATH}}"],
|
|
573
|
+
[vars["BUN_BIN_DIR"], "{{BUN_BIN_DIR}}"],
|
|
574
|
+
[vars["PATH_PREFIX"], "{{PATH_PREFIX}}"],
|
|
575
|
+
[vars["HOME_DIR"], "{{HOME_DIR}}"]
|
|
576
|
+
];
|
|
577
|
+
for (const [actual, placeholder] of replacements) {
|
|
578
|
+
next = replaceKnownPath(next, actual, placeholder);
|
|
579
|
+
}
|
|
580
|
+
next = next.replace(/~\/workspace/g, "{{WORKSPACE_ROOT}}").replace(/~\/Workspace/g, "{{WORKSPACE_ROOT}}").replace(/\/opt\/homebrew\/bin\/bun/g, "{{BUN_PATH}}");
|
|
581
|
+
return { content: next, changed: next !== content };
|
|
582
|
+
}
|
|
583
|
+
function renderMachineAwareContent(content, variables) {
|
|
584
|
+
return isTemplate(content) ? renderTemplate(content, variables) : content;
|
|
585
|
+
}
|
|
586
|
+
var BREW_BUN_PATH = "/opt/homebrew/bin/bun";
|
|
587
|
+
var init_machine = __esm(() => {
|
|
588
|
+
init_template();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// src/lib/transforms.ts
|
|
592
|
+
import { basename, extname } from "path";
|
|
593
|
+
function ensureTrailingNewline(content) {
|
|
594
|
+
return content.endsWith(`
|
|
595
|
+
`) ? content : `${content}
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
function yamlQuote(value) {
|
|
599
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
600
|
+
}
|
|
601
|
+
function headingLevel(line) {
|
|
602
|
+
const m = line.match(/^(#{1,6})\s+/);
|
|
603
|
+
return m ? m[1].length : null;
|
|
604
|
+
}
|
|
605
|
+
function stripClaudeOnlySections(content) {
|
|
606
|
+
const withoutMarkedBlocks = content.replace(/<!--\s*claude-only:start\s*-->[\s\S]*?<!--\s*claude-only:end\s*-->/gi, "");
|
|
607
|
+
const lines = withoutMarkedBlocks.split(`
|
|
608
|
+
`);
|
|
609
|
+
const kept = [];
|
|
610
|
+
let droppingUntilLevel = null;
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
const level = headingLevel(line);
|
|
613
|
+
if (level !== null && droppingUntilLevel !== null && level <= droppingUntilLevel) {
|
|
614
|
+
droppingUntilLevel = null;
|
|
615
|
+
}
|
|
616
|
+
if (droppingUntilLevel !== null)
|
|
617
|
+
continue;
|
|
618
|
+
if (/^#{1,6}\s+.*claude(?:\s+code)?[-\s]+only\b/i.test(line)) {
|
|
619
|
+
droppingUntilLevel = level ?? 1;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
kept.push(line);
|
|
623
|
+
}
|
|
624
|
+
return kept.join(`
|
|
625
|
+
`).replace(/\n{3,}/g, `
|
|
626
|
+
|
|
627
|
+
`).trim();
|
|
628
|
+
}
|
|
629
|
+
function isClaudeRuleConfig(source, candidate) {
|
|
630
|
+
if (candidate.id === source.id)
|
|
631
|
+
return false;
|
|
632
|
+
if (candidate.agent !== "claude" || candidate.category !== "rules")
|
|
633
|
+
return false;
|
|
634
|
+
return !!candidate.target_path?.includes("/rules/");
|
|
635
|
+
}
|
|
636
|
+
function ruleLabel(config) {
|
|
637
|
+
const file = config.target_path ? basename(config.target_path) : config.name;
|
|
638
|
+
return file.replace(/\.(md|mdc|markdown)$/i, "");
|
|
639
|
+
}
|
|
640
|
+
function claudeRules(source, context) {
|
|
641
|
+
return (context.configs ?? []).filter((config) => isClaudeRuleConfig(source, config)).sort((a, b) => (a.target_path ?? a.name).localeCompare(b.target_path ?? b.name));
|
|
642
|
+
}
|
|
643
|
+
function flattenWithRules(source, context) {
|
|
644
|
+
const parts = [stripClaudeOnlySections(source.content)];
|
|
645
|
+
const rules = claudeRules(source, context).map((rule) => ({ rule, content: stripClaudeOnlySections(rule.content) })).filter(({ content }) => Boolean(content)).map(({ rule, content }) => `### ${ruleLabel(rule)}
|
|
646
|
+
|
|
647
|
+
${content}`);
|
|
648
|
+
if (rules.length > 0) {
|
|
649
|
+
parts.push(`## Rules
|
|
650
|
+
|
|
651
|
+
${rules.join(`
|
|
652
|
+
|
|
653
|
+
`)}`);
|
|
654
|
+
}
|
|
655
|
+
return ensureTrailingNewline(parts.filter(Boolean).join(`
|
|
656
|
+
|
|
657
|
+
`));
|
|
658
|
+
}
|
|
659
|
+
function buildCodexAgentsMd(source, context = {}) {
|
|
660
|
+
return flattenWithRules(source, context);
|
|
661
|
+
}
|
|
662
|
+
function buildOpenCodeAgentsMd(source, context = {}) {
|
|
663
|
+
return flattenWithRules(source, context);
|
|
664
|
+
}
|
|
665
|
+
function buildCursorMdc(source) {
|
|
666
|
+
const stem = source.target_path ? basename(source.target_path, extname(source.target_path)) : source.slug;
|
|
667
|
+
const description = source.name || stem;
|
|
668
|
+
return ensureTrailingNewline([
|
|
669
|
+
"---",
|
|
670
|
+
`description: ${yamlQuote(description)}`,
|
|
671
|
+
'globs: ["**/*"]',
|
|
672
|
+
"alwaysApply: true",
|
|
673
|
+
"---",
|
|
674
|
+
"",
|
|
675
|
+
stripClaudeOnlySections(source.content)
|
|
676
|
+
].join(`
|
|
677
|
+
`));
|
|
678
|
+
}
|
|
679
|
+
function transformSkillContent(content) {
|
|
680
|
+
return ensureTrailingNewline(content.split(`
|
|
681
|
+
`).filter((line) => !/^\s*user_invocable\s*:/i.test(line)).join(`
|
|
682
|
+
`).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());
|
|
683
|
+
}
|
|
684
|
+
function applyTransform(source, output, context = {}) {
|
|
685
|
+
switch (output.transform) {
|
|
686
|
+
case "passthrough":
|
|
687
|
+
case "claude-passthrough":
|
|
688
|
+
return ensureTrailingNewline(source.content);
|
|
689
|
+
case "codex-flat":
|
|
690
|
+
return buildCodexAgentsMd(source, context);
|
|
691
|
+
case "opencode-flat":
|
|
692
|
+
return buildOpenCodeAgentsMd(source, context);
|
|
693
|
+
case "cursor-mdc":
|
|
694
|
+
return buildCursorMdc(source);
|
|
695
|
+
case "skill-neutral":
|
|
696
|
+
return transformSkillContent(source.content);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
var init_transforms = () => {};
|
|
700
|
+
|
|
701
|
+
// src/lib/apply.ts
|
|
702
|
+
var exports_apply = {};
|
|
703
|
+
__export(exports_apply, {
|
|
704
|
+
normalizeTargetPath: () => normalizeTargetPath,
|
|
705
|
+
getConfigHome: () => getConfigHome,
|
|
706
|
+
expandPath: () => expandPath,
|
|
707
|
+
applyConfigs: () => applyConfigs,
|
|
708
|
+
applyConfig: () => applyConfig
|
|
709
|
+
});
|
|
710
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
711
|
+
import { basename as basename2, dirname, resolve } from "path";
|
|
712
|
+
import { homedir as homedir2 } from "os";
|
|
713
|
+
function getConfigHome() {
|
|
714
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir2();
|
|
715
|
+
}
|
|
716
|
+
function expandPath(p) {
|
|
717
|
+
if (p.startsWith("~/")) {
|
|
718
|
+
return resolve(getConfigHome(), p.slice(2));
|
|
719
|
+
}
|
|
720
|
+
return resolve(p);
|
|
721
|
+
}
|
|
722
|
+
function normalizeTargetPath(p) {
|
|
723
|
+
const expanded = expandPath(p);
|
|
724
|
+
try {
|
|
725
|
+
return realpathSync(expanded);
|
|
726
|
+
} catch {
|
|
727
|
+
let current = expanded;
|
|
728
|
+
const missingSegments = [];
|
|
729
|
+
while (true) {
|
|
730
|
+
if (existsSync3(current)) {
|
|
731
|
+
try {
|
|
732
|
+
return resolve(realpathSync(current), ...missingSegments);
|
|
733
|
+
} catch {
|
|
734
|
+
return expanded;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const parent = dirname(current);
|
|
738
|
+
const name = basename2(current);
|
|
739
|
+
if (parent === current)
|
|
740
|
+
return expanded;
|
|
741
|
+
missingSegments.unshift(name);
|
|
742
|
+
current = parent;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
747
|
+
const renderedTargetPath = opts.vars ? renderMachineAwareContent(targetPath, opts.vars) : targetPath;
|
|
748
|
+
const renderedContent = opts.vars ? renderMachineAwareContent(content, opts.vars) : content;
|
|
749
|
+
const path = expandPath(renderedTargetPath);
|
|
750
|
+
const previousContent = existsSync3(path) ? readFileSync(path, "utf-8") : null;
|
|
751
|
+
const changed = previousContent !== renderedContent;
|
|
752
|
+
if (!opts.dryRun) {
|
|
753
|
+
const dir = dirname(path);
|
|
754
|
+
if (!existsSync3(dir)) {
|
|
755
|
+
mkdirSync2(dir, { recursive: true });
|
|
756
|
+
}
|
|
757
|
+
if (previousContent !== null && changed) {
|
|
758
|
+
const db = opts.db || getDatabase();
|
|
759
|
+
createSnapshot(config.id, previousContent, config.version, db);
|
|
760
|
+
}
|
|
761
|
+
writeFileSync(path, renderedContent, "utf-8");
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
config_id: config.id,
|
|
765
|
+
path,
|
|
766
|
+
previous_content: previousContent,
|
|
767
|
+
new_content: renderedContent,
|
|
768
|
+
dry_run: opts.dryRun ?? false,
|
|
769
|
+
changed,
|
|
770
|
+
...meta
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function isGeneratedOutputTarget(config, configs) {
|
|
774
|
+
if (!config.target_path)
|
|
775
|
+
return false;
|
|
776
|
+
const targetPath = normalizeTargetPath(config.target_path);
|
|
777
|
+
return configs.some((candidate) => candidate.id !== config.id && candidate.outputs.some((output) => normalizeTargetPath(output.target_path) === targetPath));
|
|
778
|
+
}
|
|
779
|
+
async function applyConfig(config, opts = {}) {
|
|
780
|
+
const selectedOutputs = opts.outputAgent ? config.outputs.filter((output) => output.agent === opts.outputAgent) : config.outputs;
|
|
781
|
+
const shouldApplyPrimary = !opts.outputAgent || config.agent === opts.outputAgent;
|
|
782
|
+
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
783
|
+
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
784
|
+
}
|
|
785
|
+
const db = opts.db || getDatabase();
|
|
786
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
|
|
787
|
+
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
788
|
+
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
789
|
+
}
|
|
790
|
+
const outputResults = [];
|
|
791
|
+
for (const output of selectedOutputs) {
|
|
792
|
+
outputResults.push(await applyOutput(config, output, contextConfigs, opts));
|
|
793
|
+
}
|
|
794
|
+
let result;
|
|
795
|
+
if (config.target_path && shouldApplyPrimary) {
|
|
796
|
+
result = await writeConfigResult(config, config.target_path, config.content, opts);
|
|
797
|
+
result.outputs = outputResults;
|
|
798
|
+
result.changed = result.changed || outputResults.some((output) => output.changed);
|
|
799
|
+
} else {
|
|
800
|
+
result = {
|
|
801
|
+
...outputResults[0],
|
|
802
|
+
outputs: outputResults.slice(1),
|
|
803
|
+
changed: outputResults.some((output) => output.changed)
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (!opts.dryRun) {
|
|
807
|
+
updateConfig(config.id, { synced_at: now() }, db);
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
811
|
+
async function applyOutput(config, output, contextConfigs, opts) {
|
|
812
|
+
const content = applyTransform(config, output, { configs: contextConfigs });
|
|
813
|
+
return writeConfigResult(config, output.target_path, content, opts, {
|
|
814
|
+
agent: output.agent,
|
|
815
|
+
transform: output.transform
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async function applyConfigs(configs, opts = {}) {
|
|
819
|
+
const results = [];
|
|
820
|
+
for (const config of configs) {
|
|
821
|
+
if (config.kind === "reference")
|
|
822
|
+
continue;
|
|
823
|
+
results.push(await applyConfig(config, opts));
|
|
824
|
+
}
|
|
825
|
+
return results;
|
|
826
|
+
}
|
|
827
|
+
var init_apply = __esm(() => {
|
|
828
|
+
init_types();
|
|
829
|
+
init_database();
|
|
830
|
+
init_configs();
|
|
831
|
+
init_snapshots();
|
|
832
|
+
init_machine();
|
|
833
|
+
init_transforms();
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/lib/redact.ts
|
|
837
|
+
var exports_redact = {};
|
|
838
|
+
__export(exports_redact, {
|
|
839
|
+
scanSecrets: () => scanSecrets,
|
|
840
|
+
redactContent: () => redactContent,
|
|
841
|
+
hasSecrets: () => hasSecrets
|
|
842
|
+
});
|
|
843
|
+
function redactShell(content) {
|
|
844
|
+
const redacted = [];
|
|
845
|
+
const lines = content.split(`
|
|
846
|
+
`);
|
|
847
|
+
const out = [];
|
|
848
|
+
for (let i = 0;i < lines.length; i++) {
|
|
849
|
+
const line = lines[i];
|
|
850
|
+
const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
851
|
+
if (m) {
|
|
852
|
+
const [, prefix, key, eq, quote, value] = m;
|
|
853
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
854
|
+
const reason = reasonFor(key, value);
|
|
855
|
+
redacted.push({ varName: key, line: i + 1, reason });
|
|
856
|
+
out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
out.push(line);
|
|
861
|
+
}
|
|
862
|
+
return { content: out.join(`
|
|
863
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
864
|
+
}
|
|
865
|
+
function redactJson(content) {
|
|
866
|
+
const redacted = [];
|
|
867
|
+
const lines = content.split(`
|
|
868
|
+
`);
|
|
869
|
+
const out = [];
|
|
870
|
+
for (let i = 0;i < lines.length; i++) {
|
|
871
|
+
const line = lines[i];
|
|
872
|
+
const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
|
|
873
|
+
if (m) {
|
|
874
|
+
const [, prefix, key, value, comma, trail] = m;
|
|
875
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
876
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
877
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
878
|
+
out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
let newLine = line;
|
|
883
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
884
|
+
newLine = newLine.replace(re, (match) => {
|
|
885
|
+
const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
886
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
887
|
+
return `{{${varName}}}`;
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
out.push(newLine);
|
|
891
|
+
}
|
|
892
|
+
return { content: out.join(`
|
|
893
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
894
|
+
}
|
|
895
|
+
function redactToml(content) {
|
|
896
|
+
const redacted = [];
|
|
897
|
+
const lines = content.split(`
|
|
898
|
+
`);
|
|
899
|
+
const out = [];
|
|
900
|
+
for (let i = 0;i < lines.length; i++) {
|
|
901
|
+
const line = lines[i];
|
|
902
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
903
|
+
if (m) {
|
|
904
|
+
const [, indent, key, eq, quote, value] = m;
|
|
905
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
906
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
907
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
908
|
+
out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
out.push(line);
|
|
913
|
+
}
|
|
914
|
+
return { content: out.join(`
|
|
915
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
916
|
+
}
|
|
917
|
+
function redactIni(content) {
|
|
918
|
+
const redacted = [];
|
|
919
|
+
const lines = content.split(`
|
|
920
|
+
`);
|
|
921
|
+
const out = [];
|
|
922
|
+
for (let i = 0;i < lines.length; i++) {
|
|
923
|
+
const line = lines[i];
|
|
924
|
+
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
925
|
+
if (authM && !authM[2].startsWith("{{")) {
|
|
926
|
+
redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
927
|
+
out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
931
|
+
if (m) {
|
|
932
|
+
const [, indent, key, eq, value] = m;
|
|
933
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
934
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
935
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
936
|
+
out.push(`${indent}${key}${eq}{{${varName}}}`);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
out.push(line);
|
|
941
|
+
}
|
|
942
|
+
return { content: out.join(`
|
|
943
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
944
|
+
}
|
|
945
|
+
function redactGeneric(content) {
|
|
946
|
+
const redacted = [];
|
|
947
|
+
const lines = content.split(`
|
|
948
|
+
`);
|
|
949
|
+
const out = [];
|
|
950
|
+
for (let i = 0;i < lines.length; i++) {
|
|
951
|
+
let line = lines[i];
|
|
952
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
953
|
+
line = line.replace(re, (match) => {
|
|
954
|
+
const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
955
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
956
|
+
return `{{${varName}}}`;
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
out.push(line);
|
|
960
|
+
}
|
|
961
|
+
return { content: out.join(`
|
|
962
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
963
|
+
}
|
|
964
|
+
function shouldRedactKeyValue(key, value) {
|
|
965
|
+
if (!value || value.startsWith("{{"))
|
|
966
|
+
return false;
|
|
967
|
+
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
968
|
+
return false;
|
|
969
|
+
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
970
|
+
return false;
|
|
971
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
972
|
+
return true;
|
|
973
|
+
for (const { re } of VALUE_PATTERNS) {
|
|
974
|
+
if (re.test(value))
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
function reasonFor(key, value) {
|
|
980
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
981
|
+
return `secret key name: ${key}`;
|
|
982
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
983
|
+
if (re.test(value))
|
|
984
|
+
return reason;
|
|
985
|
+
}
|
|
986
|
+
return "secret value pattern";
|
|
987
|
+
}
|
|
988
|
+
function redactContent(content, format) {
|
|
989
|
+
switch (format) {
|
|
990
|
+
case "shell":
|
|
991
|
+
return redactShell(content);
|
|
992
|
+
case "json":
|
|
993
|
+
return redactJson(content);
|
|
994
|
+
case "toml":
|
|
995
|
+
return redactToml(content);
|
|
996
|
+
case "ini":
|
|
997
|
+
return redactIni(content);
|
|
998
|
+
default:
|
|
999
|
+
return redactGeneric(content);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function scanSecrets(content, format) {
|
|
1003
|
+
const r = redactContent(content, format);
|
|
1004
|
+
return r.redacted;
|
|
1005
|
+
}
|
|
1006
|
+
function hasSecrets(content, format) {
|
|
1007
|
+
return scanSecrets(content, format).length > 0;
|
|
1008
|
+
}
|
|
1009
|
+
var SECRET_KEY_PATTERN, VALUE_PATTERNS, MIN_SECRET_VALUE_LEN = 8;
|
|
1010
|
+
var init_redact = __esm(() => {
|
|
1011
|
+
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;
|
|
1012
|
+
VALUE_PATTERNS = [
|
|
1013
|
+
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
1014
|
+
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
1015
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
1016
|
+
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
1017
|
+
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
1018
|
+
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
1019
|
+
{ re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
|
|
1020
|
+
{ re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
|
|
1021
|
+
];
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// src/lib/sync.ts
|
|
1025
|
+
var exports_sync = {};
|
|
1026
|
+
__export(exports_sync, {
|
|
1027
|
+
syncToDisk: () => syncToDisk,
|
|
1028
|
+
syncToDir: () => syncToDir,
|
|
1029
|
+
syncProject: () => syncProject,
|
|
1030
|
+
syncKnown: () => syncKnown,
|
|
1031
|
+
syncFromDir: () => syncFromDir,
|
|
1032
|
+
diffConfig: () => diffConfig,
|
|
1033
|
+
detectFormat: () => detectFormat,
|
|
1034
|
+
detectCategory: () => detectCategory,
|
|
1035
|
+
detectAgent: () => detectAgent,
|
|
1036
|
+
PROJECT_CONFIG_FILES: () => PROJECT_CONFIG_FILES,
|
|
1037
|
+
KNOWN_CONFIGS: () => KNOWN_CONFIGS,
|
|
1038
|
+
CLAUDE_PROMPT_OUTPUTS: () => CLAUDE_PROMPT_OUTPUTS
|
|
1039
|
+
});
|
|
1040
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
1041
|
+
import { basename as basename3, extname as extname2, join as join3 } from "path";
|
|
1042
|
+
function claudeRuleOutputs(fileName) {
|
|
1043
|
+
const stem = basename3(fileName, extname2(fileName));
|
|
1044
|
+
return [
|
|
1045
|
+
{ agent: "cursor", target_path: `~/.cursor/rules/${stem}.mdc`, transform: "cursor-mdc" }
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
function normalizeOutputs(outputs) {
|
|
1049
|
+
return outputs ?? [];
|
|
1050
|
+
}
|
|
1051
|
+
function outputsEqual(a, b) {
|
|
1052
|
+
return JSON.stringify(normalizeOutputs(a)) === JSON.stringify(normalizeOutputs(b));
|
|
1053
|
+
}
|
|
1054
|
+
function outputOwnerIdsByTarget(configs) {
|
|
1055
|
+
const owners = new Map;
|
|
1056
|
+
for (const config of configs) {
|
|
1057
|
+
for (const output of config.outputs) {
|
|
1058
|
+
const targetPath = normalizeTargetPath(output.target_path);
|
|
1059
|
+
const existing = owners.get(targetPath) ?? new Set;
|
|
1060
|
+
existing.add(config.id);
|
|
1061
|
+
owners.set(targetPath, existing);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return owners;
|
|
1065
|
+
}
|
|
1066
|
+
function isGeneratedOutputTarget2(config, owners) {
|
|
1067
|
+
if (!config.target_path)
|
|
1068
|
+
return false;
|
|
1069
|
+
const ownerIds = owners.get(normalizeTargetPath(config.target_path));
|
|
1070
|
+
return !!ownerIds && !ownerIds.has(config.id);
|
|
1071
|
+
}
|
|
1072
|
+
function hasClaudePromptSource() {
|
|
1073
|
+
return existsSync4(expandPath("~/.claude/CLAUDE.md"));
|
|
1074
|
+
}
|
|
1075
|
+
function hasClaudeRuleSourceForCursorTarget(targetPath) {
|
|
1076
|
+
const absoluteTargetPath = expandPath(targetPath);
|
|
1077
|
+
const absolutePrefix = expandPath("~/.cursor/rules");
|
|
1078
|
+
if (!absoluteTargetPath.startsWith(`${absolutePrefix}/`) || !absoluteTargetPath.endsWith(".mdc"))
|
|
1079
|
+
return false;
|
|
1080
|
+
const stem = basename3(absoluteTargetPath, ".mdc");
|
|
1081
|
+
return existsSync4(expandPath(`~/.claude/rules/${stem}.md`)) || existsSync4(expandPath(`~/.claude/rules/${stem}.mdc`));
|
|
1082
|
+
}
|
|
1083
|
+
function isKnownGeneratedTargetPath(targetPath) {
|
|
1084
|
+
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
1085
|
+
const promptTargets = new Set(CLAUDE_PROMPT_OUTPUTS.map((output) => normalizeTargetPath(output.target_path)));
|
|
1086
|
+
if (promptTargets.has(normalizedTargetPath))
|
|
1087
|
+
return hasClaudePromptSource();
|
|
1088
|
+
return hasClaudeRuleSourceForCursorTarget(targetPath);
|
|
1089
|
+
}
|
|
1090
|
+
async function syncProject(opts) {
|
|
1091
|
+
const d = opts.db || getDatabase();
|
|
1092
|
+
const absDir = expandPath(opts.projectDir);
|
|
1093
|
+
const projectName = absDir.split("/").pop() || "project";
|
|
1094
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1095
|
+
const allConfigs = listConfigs(undefined, d);
|
|
1096
|
+
const machine = detectMachineContext();
|
|
1097
|
+
for (const pf of PROJECT_CONFIG_FILES) {
|
|
1098
|
+
const abs = join3(absDir, pf.file);
|
|
1099
|
+
if (!existsSync4(abs))
|
|
1100
|
+
continue;
|
|
1101
|
+
try {
|
|
1102
|
+
const rawContent = readFileSync2(abs, "utf-8");
|
|
1103
|
+
if (rawContent.length > 500000) {
|
|
1104
|
+
result.skipped.push(pf.file);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const redacted = redactContent(rawContent, pf.format);
|
|
1108
|
+
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
1109
|
+
const content = machineAware.content;
|
|
1110
|
+
const isTemplate2 = redacted.isTemplate || machineAware.changed;
|
|
1111
|
+
const name = `${projectName}/${pf.file}`;
|
|
1112
|
+
const targetPath = abs.startsWith(getConfigHome()) ? abs.replace(getConfigHome(), "~") : abs;
|
|
1113
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1114
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1115
|
+
if (!existing) {
|
|
1116
|
+
if (!opts.dryRun)
|
|
1117
|
+
createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
|
|
1118
|
+
result.added++;
|
|
1119
|
+
} else if (existing.content !== content) {
|
|
1120
|
+
if (!opts.dryRun)
|
|
1121
|
+
updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
|
|
1122
|
+
result.updated++;
|
|
1123
|
+
} else {
|
|
1124
|
+
result.unchanged++;
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
result.skipped.push(pf.file);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const rulesDir = join3(absDir, ".claude", "rules");
|
|
1131
|
+
if (existsSync4(rulesDir)) {
|
|
1132
|
+
const mdFiles = readdirSync(rulesDir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
1133
|
+
for (const f of mdFiles) {
|
|
1134
|
+
const abs = join3(rulesDir, f);
|
|
1135
|
+
const raw = readFileSync2(abs, "utf-8");
|
|
1136
|
+
const redacted = redactContent(raw, "markdown");
|
|
1137
|
+
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
1138
|
+
const content = machineAware.content;
|
|
1139
|
+
const isTemplate2 = redacted.isTemplate || machineAware.changed;
|
|
1140
|
+
const name = `${projectName}/rules/${f}`;
|
|
1141
|
+
const targetPath = abs.startsWith(getConfigHome()) ? abs.replace(getConfigHome(), "~") : abs;
|
|
1142
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1143
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1144
|
+
if (!existing) {
|
|
1145
|
+
if (!opts.dryRun)
|
|
1146
|
+
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
|
|
1147
|
+
result.added++;
|
|
1148
|
+
} else if (existing.content !== content) {
|
|
1149
|
+
if (!opts.dryRun)
|
|
1150
|
+
updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
|
|
1151
|
+
result.updated++;
|
|
1152
|
+
} else {
|
|
1153
|
+
result.unchanged++;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
async function syncKnown(opts = {}) {
|
|
1160
|
+
const d = opts.db || getDatabase();
|
|
1161
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1162
|
+
const home = getConfigHome();
|
|
1163
|
+
const machine = detectMachineContext();
|
|
1164
|
+
let targets = KNOWN_CONFIGS;
|
|
1165
|
+
if (opts.agent)
|
|
1166
|
+
targets = targets.filter((k) => k.agent === opts.agent);
|
|
1167
|
+
if (opts.category)
|
|
1168
|
+
targets = targets.filter((k) => k.category === opts.category);
|
|
1169
|
+
const allConfigs = listConfigs(undefined, d);
|
|
1170
|
+
const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
|
|
1171
|
+
for (const known of targets) {
|
|
1172
|
+
if (known.rulesDir) {
|
|
1173
|
+
const absDir = expandPath(known.rulesDir);
|
|
1174
|
+
if (!existsSync4(absDir)) {
|
|
1175
|
+
result.skipped.push(known.rulesDir);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
const extensions = known.rulesExtensions ?? [".md", ".mdc"];
|
|
1179
|
+
const ruleFiles = readdirSync(absDir).filter((f) => extensions.some((ext) => f.endsWith(ext)));
|
|
1180
|
+
for (const f of ruleFiles) {
|
|
1181
|
+
const abs2 = join3(absDir, f);
|
|
1182
|
+
const targetPath = abs2.replace(home, "~");
|
|
1183
|
+
if (existingOutputOwners.has(normalizeTargetPath(targetPath)) || isKnownGeneratedTargetPath(targetPath)) {
|
|
1184
|
+
result.skipped.push(`${targetPath} (generated output)`);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const raw = readFileSync2(abs2, "utf-8");
|
|
1188
|
+
const redacted = redactContent(raw, "markdown");
|
|
1189
|
+
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
1190
|
+
const content = machineAware.content;
|
|
1191
|
+
const isTemplate2 = redacted.isTemplate || machineAware.changed;
|
|
1192
|
+
const name = `${known.name}-${f}`;
|
|
1193
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1194
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1195
|
+
const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
|
|
1196
|
+
if (!existing) {
|
|
1197
|
+
if (!opts.dryRun)
|
|
1198
|
+
createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
|
|
1199
|
+
result.added++;
|
|
1200
|
+
} else if (existing.content !== content) {
|
|
1201
|
+
if (!opts.dryRun)
|
|
1202
|
+
updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
|
|
1203
|
+
result.updated++;
|
|
1204
|
+
} else if (!outputsEqual(existing.outputs, outputs)) {
|
|
1205
|
+
if (!opts.dryRun)
|
|
1206
|
+
updateConfig(existing.id, { outputs }, d);
|
|
1207
|
+
result.updated++;
|
|
1208
|
+
} else {
|
|
1209
|
+
result.unchanged++;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const abs = expandPath(known.path);
|
|
1215
|
+
if (!existsSync4(abs)) {
|
|
1216
|
+
result.skipped.push(known.path);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const rawContent = readFileSync2(abs, "utf-8");
|
|
1221
|
+
if (rawContent.length > 500000) {
|
|
1222
|
+
result.skipped.push(known.path + " (too large)");
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const fmt = known.format ?? detectFormat(abs);
|
|
1226
|
+
const redacted = redactContent(rawContent, fmt);
|
|
1227
|
+
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
1228
|
+
const content = machineAware.content;
|
|
1229
|
+
const isTemplate2 = redacted.isTemplate || machineAware.changed;
|
|
1230
|
+
const targetPath = abs.replace(home, "~");
|
|
1231
|
+
if (existingOutputOwners.has(normalizeTargetPath(targetPath)) || isKnownGeneratedTargetPath(targetPath)) {
|
|
1232
|
+
result.skipped.push(`${targetPath} (generated output)`);
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
1236
|
+
if (!existing) {
|
|
1237
|
+
if (!opts.dryRun) {
|
|
1238
|
+
createConfig({
|
|
1239
|
+
name: known.name,
|
|
1240
|
+
category: known.category,
|
|
1241
|
+
agent: known.agent,
|
|
1242
|
+
format: fmt,
|
|
1243
|
+
content,
|
|
1244
|
+
target_path: known.kind === "reference" ? null : targetPath,
|
|
1245
|
+
kind: known.kind ?? "file",
|
|
1246
|
+
description: known.description,
|
|
1247
|
+
is_template: isTemplate2,
|
|
1248
|
+
outputs: known.outputs
|
|
1249
|
+
}, d);
|
|
1250
|
+
}
|
|
1251
|
+
result.added++;
|
|
1252
|
+
} else if (existing.content !== content) {
|
|
1253
|
+
if (!opts.dryRun)
|
|
1254
|
+
updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
|
|
1255
|
+
result.updated++;
|
|
1256
|
+
} else if (!outputsEqual(existing.outputs, known.outputs)) {
|
|
1257
|
+
if (!opts.dryRun)
|
|
1258
|
+
updateConfig(existing.id, { outputs: known.outputs }, d);
|
|
1259
|
+
result.updated++;
|
|
1260
|
+
} else {
|
|
1261
|
+
result.unchanged++;
|
|
1262
|
+
}
|
|
1263
|
+
} catch {
|
|
1264
|
+
result.skipped.push(known.path);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1269
|
+
async function syncToDisk(opts = {}) {
|
|
1270
|
+
const d = opts.db || getDatabase();
|
|
1271
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1272
|
+
const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
|
|
1273
|
+
const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
|
|
1274
|
+
let configs = allFileConfigs.filter((config) => {
|
|
1275
|
+
return !isGeneratedOutputTarget2(config, outputOwners);
|
|
1276
|
+
}).filter((config) => {
|
|
1277
|
+
if (!opts.agent)
|
|
1278
|
+
return true;
|
|
1279
|
+
return config.agent === opts.agent || config.outputs.some((output) => output.agent === opts.agent);
|
|
1280
|
+
});
|
|
1281
|
+
for (const config of configs) {
|
|
1282
|
+
if (!config.target_path && config.outputs.length === 0)
|
|
1283
|
+
continue;
|
|
1284
|
+
try {
|
|
1285
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
|
|
1286
|
+
r.changed ? result.updated++ : result.unchanged++;
|
|
1287
|
+
} catch {
|
|
1288
|
+
result.skipped.push(config.target_path ?? config.id);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return result;
|
|
1292
|
+
}
|
|
1293
|
+
function buildDiff(expectedContent, targetPath) {
|
|
1294
|
+
const path = expandPath(targetPath);
|
|
1295
|
+
if (!existsSync4(path))
|
|
1296
|
+
return `(file not found on disk: ${path})`;
|
|
1297
|
+
const diskContent = readFileSync2(path, "utf-8");
|
|
1298
|
+
if (diskContent === expectedContent)
|
|
1299
|
+
return "(no diff \u2014 identical)";
|
|
1300
|
+
const stored = expectedContent.split(`
|
|
1301
|
+
`);
|
|
1302
|
+
const disk = diskContent.split(`
|
|
1303
|
+
`);
|
|
1304
|
+
const lines = [`--- stored (DB)`, `+++ disk (${path})`];
|
|
1305
|
+
const maxLen = Math.max(stored.length, disk.length);
|
|
1306
|
+
for (let i = 0;i < maxLen; i++) {
|
|
1307
|
+
const s = stored[i];
|
|
1308
|
+
const dk = disk[i];
|
|
1309
|
+
if (s === dk) {
|
|
1310
|
+
if (s !== undefined)
|
|
1311
|
+
lines.push(` ${s}`);
|
|
1312
|
+
} else {
|
|
1313
|
+
if (s !== undefined)
|
|
1314
|
+
lines.push(`-${s}`);
|
|
1315
|
+
if (dk !== undefined)
|
|
1316
|
+
lines.push(`+${dk}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return lines.join(`
|
|
1320
|
+
`);
|
|
1321
|
+
}
|
|
1322
|
+
function diffConfig(config, opts = {}) {
|
|
1323
|
+
if (!config.target_path && config.outputs.length === 0)
|
|
1324
|
+
return "(reference \u2014 no target path)";
|
|
1325
|
+
const diffs = [];
|
|
1326
|
+
const db = opts.db || getDatabase();
|
|
1327
|
+
const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
|
|
1328
|
+
if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
|
|
1329
|
+
return "(generated output \u2014 managed by fan-out)";
|
|
1330
|
+
}
|
|
1331
|
+
if (config.target_path) {
|
|
1332
|
+
const diff = buildDiff(config.content, config.target_path);
|
|
1333
|
+
if (!diff.includes("no diff"))
|
|
1334
|
+
diffs.push(diff);
|
|
1335
|
+
}
|
|
1336
|
+
if (config.outputs.length > 0) {
|
|
1337
|
+
for (const output of config.outputs) {
|
|
1338
|
+
const expected = applyTransform(config, output, { configs: contextConfigs });
|
|
1339
|
+
const diff = buildDiff(expected, output.target_path);
|
|
1340
|
+
if (!diff.includes("no diff"))
|
|
1341
|
+
diffs.push(diff);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return diffs.length > 0 ? diffs.join(`
|
|
1345
|
+
|
|
1346
|
+
`) : "(no diff \u2014 identical)";
|
|
1347
|
+
}
|
|
1348
|
+
function detectCategory(filePath) {
|
|
1349
|
+
const p = filePath.toLowerCase().replace(getConfigHome(), "~");
|
|
1350
|
+
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"))
|
|
1351
|
+
return "rules";
|
|
1352
|
+
if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/") || p.includes("/.config/opencode/") || p.includes("/.codewith/") || p.includes("/.config/aicopilot/"))
|
|
1353
|
+
return "agent";
|
|
1354
|
+
if (p.includes(".mcp.json") || p.includes("mcp"))
|
|
1355
|
+
return "mcp";
|
|
1356
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
|
|
1357
|
+
return "shell";
|
|
1358
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
1359
|
+
return "git";
|
|
1360
|
+
if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
|
|
1361
|
+
return "tools";
|
|
1362
|
+
if (p.includes(".secrets"))
|
|
1363
|
+
return "secrets_schema";
|
|
1364
|
+
return "tools";
|
|
1365
|
+
}
|
|
1366
|
+
function detectAgent(filePath) {
|
|
1367
|
+
const p = filePath.toLowerCase().replace(getConfigHome(), "~");
|
|
1368
|
+
if (p.includes("/.claude/") || p.endsWith("claude.md"))
|
|
1369
|
+
return "claude";
|
|
1370
|
+
if (p.includes("/.config/opencode/"))
|
|
1371
|
+
return "opencode";
|
|
1372
|
+
if (p.includes("/.cursor/") || p.endsWith(".mdc"))
|
|
1373
|
+
return "cursor";
|
|
1374
|
+
if (p.includes("/.codewith/") || p.endsWith("codewith.md"))
|
|
1375
|
+
return "codewith";
|
|
1376
|
+
if (p.includes("/.config/aicopilot/"))
|
|
1377
|
+
return "aicopilot";
|
|
1378
|
+
if (p.includes("/.codex/") || p.endsWith("agents.md"))
|
|
1379
|
+
return "codex";
|
|
1380
|
+
if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
|
|
1381
|
+
return "gemini";
|
|
1382
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
|
|
1383
|
+
return "zsh";
|
|
1384
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
1385
|
+
return "git";
|
|
1386
|
+
if (p.includes(".npmrc"))
|
|
1387
|
+
return "npm";
|
|
1388
|
+
return "global";
|
|
1389
|
+
}
|
|
1390
|
+
function detectFormat(filePath) {
|
|
1391
|
+
const ext = extname2(filePath).toLowerCase();
|
|
1392
|
+
if (ext === ".json")
|
|
1393
|
+
return "json";
|
|
1394
|
+
if (ext === ".toml")
|
|
1395
|
+
return "toml";
|
|
1396
|
+
if (ext === ".yaml" || ext === ".yml")
|
|
1397
|
+
return "yaml";
|
|
1398
|
+
if (ext === ".md" || ext === ".mdc" || ext === ".markdown")
|
|
1399
|
+
return "markdown";
|
|
1400
|
+
if (ext === ".ini" || ext === ".cfg")
|
|
1401
|
+
return "ini";
|
|
1402
|
+
return "text";
|
|
1403
|
+
}
|
|
1404
|
+
var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
|
|
1405
|
+
var init_sync = __esm(() => {
|
|
1406
|
+
init_database();
|
|
1407
|
+
init_configs();
|
|
1408
|
+
init_apply();
|
|
1409
|
+
init_redact();
|
|
1410
|
+
init_machine();
|
|
1411
|
+
init_transforms();
|
|
1412
|
+
init_sync_dir();
|
|
1413
|
+
CLAUDE_PROMPT_OUTPUTS = [
|
|
1414
|
+
{ agent: "codex", target_path: "~/.codex/AGENTS.md", transform: "codex-flat" },
|
|
1415
|
+
{ agent: "codewith", target_path: "~/.codewith/CODEWITH.md", transform: "codex-flat" },
|
|
1416
|
+
{ agent: "opencode", target_path: "~/.config/opencode/AGENTS.md", transform: "opencode-flat" },
|
|
1417
|
+
{ agent: "aicopilot", target_path: "~/.config/aicopilot/AGENTS.md", transform: "opencode-flat" },
|
|
1418
|
+
{ agent: "cursor", target_path: "~/.cursor/rules/claude.mdc", transform: "cursor-mdc" }
|
|
1419
|
+
];
|
|
1420
|
+
KNOWN_CONFIGS = [
|
|
1421
|
+
{ path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown", outputs: CLAUDE_PROMPT_OUTPUTS },
|
|
1422
|
+
{ path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
|
|
1423
|
+
{ path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
|
|
1424
|
+
{ path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json", optional: true },
|
|
1425
|
+
{ path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules", rulesExtensions: [".md", ".mdc"] },
|
|
1426
|
+
{ path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
|
|
1427
|
+
{ path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
|
|
1428
|
+
{ path: "~/.config/opencode/AGENTS.md", name: "opencode-agents-md", category: "rules", agent: "opencode", format: "markdown", optional: true },
|
|
1429
|
+
{ path: "~/.config/opencode/opencode.json", name: "opencode-config", category: "mcp", agent: "opencode", format: "json", optional: true, description: "OpenCode config (includes Skills MCP server entries)" },
|
|
1430
|
+
{ path: "~/.cursor/rules", name: "cursor-rules", category: "rules", agent: "cursor", rulesDir: "~/.cursor/rules", rulesExtensions: [".md", ".mdc"] },
|
|
1431
|
+
{ path: "~/.cursor/mcp.json", name: "cursor-mcp", category: "mcp", agent: "cursor", format: "json", optional: true, description: "Cursor MCP config (includes Skills MCP server entries)" },
|
|
1432
|
+
{ path: "~/.codewith/CODEWITH.md", name: "codewith-codewith-md", category: "rules", agent: "codewith", format: "markdown", optional: true },
|
|
1433
|
+
{ 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)" },
|
|
1434
|
+
{ path: "~/.config/aicopilot/AGENTS.md", name: "aicopilot-agents-md", category: "rules", agent: "aicopilot", format: "markdown", optional: true },
|
|
1435
|
+
{ path: "~/.config/aicopilot/opencode.json", name: "aicopilot-config", category: "mcp", agent: "aicopilot", format: "json", optional: true, description: "AI Copilot config (OpenCode fork, includes Skills MCP server entries)" },
|
|
1436
|
+
{ path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
|
|
1437
|
+
{ path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown", optional: true },
|
|
1438
|
+
{ path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
|
|
1439
|
+
{ path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
|
|
1440
|
+
{ path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh", optional: true },
|
|
1441
|
+
{ path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh", optional: true },
|
|
1442
|
+
{ path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh", optional: true },
|
|
1443
|
+
{ path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
|
|
1444
|
+
{ path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git", optional: true },
|
|
1445
|
+
{ path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
|
|
1446
|
+
{ path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml", optional: true }
|
|
1447
|
+
];
|
|
1448
|
+
PROJECT_CONFIG_FILES = [
|
|
1449
|
+
{ file: "CLAUDE.md", category: "rules", agent: "claude", format: "markdown" },
|
|
1450
|
+
{ file: ".claude/settings.json", category: "agent", agent: "claude", format: "json" },
|
|
1451
|
+
{ file: ".claude/settings.local.json", category: "agent", agent: "claude", format: "json" },
|
|
1452
|
+
{ file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
|
|
1453
|
+
{ file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
|
|
1454
|
+
{ file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
|
|
1455
|
+
{ file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" },
|
|
1456
|
+
{ file: ".opencode/AGENTS.md", category: "rules", agent: "opencode", format: "markdown" },
|
|
1457
|
+
{ file: ".codewith/CODEWITH.md", category: "rules", agent: "codewith", format: "markdown" },
|
|
1458
|
+
{ file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
|
|
1459
|
+
];
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// src/lib/sync-dir.ts
|
|
1463
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
1464
|
+
import { join as join4, relative } from "path";
|
|
1465
|
+
import { homedir as homedir3 } from "os";
|
|
1466
|
+
function shouldSkip(p) {
|
|
1467
|
+
return SKIP.some((s) => p.includes(s));
|
|
1468
|
+
}
|
|
1469
|
+
async function syncFromDir(dir, opts = {}) {
|
|
1470
|
+
const d = opts.db || getDatabase();
|
|
1471
|
+
const absDir = expandPath(dir);
|
|
1472
|
+
if (!existsSync5(absDir))
|
|
1473
|
+
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
1474
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
1475
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1476
|
+
const home = homedir3();
|
|
1477
|
+
const allConfigs = listConfigs(undefined, d);
|
|
1478
|
+
for (const file of files) {
|
|
1479
|
+
if (shouldSkip(file)) {
|
|
1480
|
+
result.skipped.push(file);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const content = readFileSync3(file, "utf-8");
|
|
1485
|
+
if (content.length > 500000) {
|
|
1486
|
+
result.skipped.push(file + " (too large)");
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const targetPath = file.replace(home, "~");
|
|
1490
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
1491
|
+
if (!existing) {
|
|
1492
|
+
if (!opts.dryRun)
|
|
1493
|
+
createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
|
|
1494
|
+
result.added++;
|
|
1495
|
+
} else if (existing.content !== content) {
|
|
1496
|
+
if (!opts.dryRun)
|
|
1497
|
+
updateConfig(existing.id, { content }, d);
|
|
1498
|
+
result.updated++;
|
|
1499
|
+
} else {
|
|
1500
|
+
result.unchanged++;
|
|
1501
|
+
}
|
|
1502
|
+
} catch {
|
|
1503
|
+
result.skipped.push(file);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
async function syncToDir(dir, opts = {}) {
|
|
1509
|
+
const d = opts.db || getDatabase();
|
|
1510
|
+
const home = homedir3();
|
|
1511
|
+
const absDir = expandPath(dir);
|
|
1512
|
+
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
1513
|
+
const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
1514
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1515
|
+
for (const config of configs) {
|
|
1516
|
+
if (config.kind === "reference")
|
|
1517
|
+
continue;
|
|
1518
|
+
try {
|
|
1519
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
|
|
1520
|
+
r.changed ? result.updated++ : result.unchanged++;
|
|
1521
|
+
} catch {
|
|
1522
|
+
result.skipped.push(config.target_path || config.id);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
1527
|
+
function walkDir(dir, files = []) {
|
|
1528
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1529
|
+
const full = join4(dir, entry.name);
|
|
1530
|
+
if (shouldSkip(full))
|
|
1531
|
+
continue;
|
|
1532
|
+
if (entry.isDirectory())
|
|
1533
|
+
walkDir(full, files);
|
|
1534
|
+
else if (entry.isFile())
|
|
1535
|
+
files.push(full);
|
|
1536
|
+
}
|
|
1537
|
+
return files;
|
|
1538
|
+
}
|
|
1539
|
+
var SKIP;
|
|
1540
|
+
var init_sync_dir = __esm(() => {
|
|
1541
|
+
init_database();
|
|
1542
|
+
init_configs();
|
|
1543
|
+
init_apply();
|
|
1544
|
+
init_sync();
|
|
1545
|
+
SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
// package.json
|
|
1549
|
+
var require_package = __commonJS((exports, module) => {
|
|
1550
|
+
module.exports = {
|
|
1551
|
+
name: "@hasna/instructions",
|
|
1552
|
+
version: "0.3.0",
|
|
1553
|
+
description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
|
|
1554
|
+
type: "module",
|
|
1555
|
+
main: "dist/index.js",
|
|
1556
|
+
types: "dist/index.d.ts",
|
|
1557
|
+
bin: {
|
|
1558
|
+
instructions: "dist/cli/index.js",
|
|
1559
|
+
"instructions-mcp": "dist/mcp/index.js",
|
|
1560
|
+
"instructions-serve": "dist/server/index.js",
|
|
1561
|
+
"configs-mcp": "dist/mcp/index.js"
|
|
1562
|
+
},
|
|
1563
|
+
exports: {
|
|
1564
|
+
".": {
|
|
1565
|
+
types: "./dist/index.d.ts",
|
|
1566
|
+
import: "./dist/index.js"
|
|
1567
|
+
},
|
|
1568
|
+
"./storage": {
|
|
1569
|
+
types: "./dist/storage.d.ts",
|
|
1570
|
+
import: "./dist/storage.js"
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
files: [
|
|
1574
|
+
"dist",
|
|
1575
|
+
"dashboard/dist",
|
|
1576
|
+
"LICENSE",
|
|
1577
|
+
"README.md"
|
|
1578
|
+
],
|
|
1579
|
+
scripts: {
|
|
1580
|
+
clean: "rm -rf dist",
|
|
1581
|
+
build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts src/storage.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
|
|
1582
|
+
"build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts src/storage.ts --outdir dist --target bun --external pg",
|
|
1583
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
1584
|
+
migrate: "bun run src/server/index.ts migrate",
|
|
1585
|
+
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
1586
|
+
"kit:check": "bunx @hasna/contracts vendor-kit --check",
|
|
1587
|
+
typecheck: "tsc --noEmit",
|
|
1588
|
+
test: "bun test",
|
|
1589
|
+
"dev:cli": "bun run src/cli/index.tsx",
|
|
1590
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
1591
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
1592
|
+
seed: "bun run scripts/seed.ts",
|
|
1593
|
+
prepublishOnly: "bun run build",
|
|
1594
|
+
postinstall: "mkdir -p $HOME/.hasna/instructions $HOME/.hasna/instructions/backups 2>/dev/null || true"
|
|
1595
|
+
},
|
|
1596
|
+
keywords: [
|
|
1597
|
+
"instructions",
|
|
1598
|
+
"configs",
|
|
1599
|
+
"dotfiles",
|
|
1600
|
+
"agent-config",
|
|
1601
|
+
"claude",
|
|
1602
|
+
"codex",
|
|
1603
|
+
"gemini",
|
|
1604
|
+
"mcp",
|
|
1605
|
+
"ai",
|
|
1606
|
+
"coding-agent",
|
|
1607
|
+
"cli",
|
|
1608
|
+
"dashboard"
|
|
1609
|
+
],
|
|
1610
|
+
publishConfig: {
|
|
1611
|
+
registry: "https://registry.npmjs.org",
|
|
1612
|
+
access: "public"
|
|
1613
|
+
},
|
|
1614
|
+
repository: {
|
|
1615
|
+
type: "git",
|
|
1616
|
+
url: "git+https://github.com/hasna/instructions.git"
|
|
1617
|
+
},
|
|
1618
|
+
homepage: "https://github.com/hasna/instructions",
|
|
1619
|
+
bugs: {
|
|
1620
|
+
url: "https://github.com/hasna/instructions/issues"
|
|
1621
|
+
},
|
|
1622
|
+
engines: {
|
|
1623
|
+
bun: ">=1.0.0"
|
|
1624
|
+
},
|
|
1625
|
+
author: "Andrei Hasna <andrei@hasna.com>",
|
|
1626
|
+
license: "Apache-2.0",
|
|
1627
|
+
dependencies: {
|
|
1628
|
+
"@hasna/contracts": "^0.4.1",
|
|
1629
|
+
"@hasna/events": "^0.1.6",
|
|
1630
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
1631
|
+
chalk: "^5.4.1",
|
|
1632
|
+
commander: "^13.1.0",
|
|
1633
|
+
hono: "^4.7.4",
|
|
1634
|
+
ink: "^5.2.0",
|
|
1635
|
+
pg: "^8.13.3",
|
|
1636
|
+
react: "^18.3.1",
|
|
1637
|
+
zod: "^3.24.2"
|
|
1638
|
+
},
|
|
1639
|
+
devDependencies: {
|
|
1640
|
+
"@types/bun": "^1.2.4",
|
|
1641
|
+
"@types/pg": "^8.11.11",
|
|
1642
|
+
"@types/react": "^18.3.18",
|
|
1643
|
+
typescript: "^5.7.3"
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
// src/mcp/index.ts
|
|
1649
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1650
|
+
|
|
1651
|
+
// src/mcp/server.ts
|
|
1652
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1653
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1654
|
+
|
|
1655
|
+
// src/db/storage-sync.ts
|
|
1656
|
+
init_database();
|
|
1657
|
+
|
|
1658
|
+
// src/db/pg-migrations.ts
|
|
1659
|
+
var PG_MIGRATIONS = [
|
|
1660
|
+
`CREATE TABLE IF NOT EXISTS configs (
|
|
1661
|
+
id TEXT PRIMARY KEY,
|
|
1662
|
+
name TEXT NOT NULL,
|
|
1663
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1664
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
1665
|
+
category TEXT NOT NULL,
|
|
1666
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
1667
|
+
target_path TEXT,
|
|
1668
|
+
outputs TEXT NOT NULL DEFAULT '[]',
|
|
1669
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
1670
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1671
|
+
description TEXT,
|
|
1672
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
1673
|
+
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
1674
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1675
|
+
created_at TEXT NOT NULL,
|
|
1676
|
+
updated_at TEXT NOT NULL,
|
|
1677
|
+
synced_at TEXT
|
|
1678
|
+
)`,
|
|
1679
|
+
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
1680
|
+
id TEXT PRIMARY KEY,
|
|
1681
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1682
|
+
content TEXT NOT NULL,
|
|
1683
|
+
version INTEGER NOT NULL,
|
|
1684
|
+
created_at TEXT NOT NULL
|
|
1685
|
+
)`,
|
|
1686
|
+
`CREATE TABLE IF NOT EXISTS profiles (
|
|
1687
|
+
id TEXT PRIMARY KEY,
|
|
1688
|
+
name TEXT NOT NULL,
|
|
1689
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1690
|
+
description TEXT,
|
|
1691
|
+
selectors TEXT NOT NULL DEFAULT '{}',
|
|
1692
|
+
variables TEXT NOT NULL DEFAULT '{}',
|
|
1693
|
+
created_at TEXT NOT NULL,
|
|
1694
|
+
updated_at TEXT NOT NULL
|
|
1695
|
+
)`,
|
|
1696
|
+
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
1697
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
1698
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1699
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1700
|
+
PRIMARY KEY (profile_id, config_id)
|
|
1701
|
+
)`,
|
|
1702
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1703
|
+
id TEXT PRIMARY KEY,
|
|
1704
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
1705
|
+
os TEXT,
|
|
1706
|
+
arch TEXT,
|
|
1707
|
+
last_applied_at TEXT,
|
|
1708
|
+
created_at TEXT NOT NULL
|
|
1709
|
+
)`,
|
|
1710
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1711
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1712
|
+
message TEXT NOT NULL,
|
|
1713
|
+
email TEXT,
|
|
1714
|
+
category TEXT DEFAULT 'general',
|
|
1715
|
+
version TEXT,
|
|
1716
|
+
machine_id TEXT,
|
|
1717
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1718
|
+
)`,
|
|
1719
|
+
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
1720
|
+
];
|
|
1721
|
+
|
|
1722
|
+
// src/db/remote-storage.ts
|
|
1723
|
+
import pg from "pg";
|
|
1724
|
+
var DISABLED_SSL_MODE = "disable";
|
|
1725
|
+
function translatePlaceholders(sql) {
|
|
1726
|
+
let index = 0;
|
|
1727
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
1728
|
+
}
|
|
1729
|
+
function normalizeParams(params) {
|
|
1730
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
1731
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
1732
|
+
}
|
|
1733
|
+
function normalizeHost(hostname) {
|
|
1734
|
+
const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
|
|
1735
|
+
try {
|
|
1736
|
+
return decodeURIComponent(stripped).toLowerCase();
|
|
1737
|
+
} catch {
|
|
1738
|
+
return stripped.toLowerCase();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function isLocalPostgresHost(hostname) {
|
|
1742
|
+
const host = normalizeHost(hostname);
|
|
1743
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
|
|
1744
|
+
}
|
|
1745
|
+
function effectivePgHost(url) {
|
|
1746
|
+
const hosts = url.searchParams.getAll("host");
|
|
1747
|
+
const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
|
|
1748
|
+
return finalHost?.trim() ? finalHost : url.hostname;
|
|
1749
|
+
}
|
|
1750
|
+
function buildPgPoolConfig(connectionString) {
|
|
1751
|
+
let url;
|
|
1752
|
+
try {
|
|
1753
|
+
url = new URL(connectionString);
|
|
1754
|
+
} catch {
|
|
1755
|
+
throw new Error("Invalid PostgreSQL connection string");
|
|
1756
|
+
}
|
|
1757
|
+
const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
|
|
1758
|
+
const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
|
|
1759
|
+
const isLocal = isLocalPostgresHost(effectivePgHost(url));
|
|
1760
|
+
const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
|
|
1761
|
+
if (!isLocal && hasDisabledSsl) {
|
|
1762
|
+
throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
|
|
1763
|
+
}
|
|
1764
|
+
const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
|
|
1765
|
+
url.searchParams.delete("sslmode");
|
|
1766
|
+
url.searchParams.delete("ssl");
|
|
1767
|
+
return {
|
|
1768
|
+
connectionString: url.toString(),
|
|
1769
|
+
ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
class PgAdapterAsync {
|
|
1774
|
+
pool;
|
|
1775
|
+
constructor(connectionString) {
|
|
1776
|
+
this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
|
|
1777
|
+
}
|
|
1778
|
+
async run(sql, ...params) {
|
|
1779
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1780
|
+
return { changes: result.rowCount ?? 0 };
|
|
1781
|
+
}
|
|
1782
|
+
async all(sql, ...params) {
|
|
1783
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1784
|
+
return result.rows;
|
|
1785
|
+
}
|
|
1786
|
+
async close() {
|
|
1787
|
+
await this.pool.end();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/db/storage-sync.ts
|
|
1792
|
+
var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
|
|
1793
|
+
var PRIMARY_KEYS = {
|
|
1794
|
+
configs: ["id"],
|
|
1795
|
+
config_snapshots: ["id"],
|
|
1796
|
+
profiles: ["id"],
|
|
1797
|
+
profile_configs: ["profile_id", "config_id"],
|
|
1798
|
+
machines: ["id"],
|
|
1799
|
+
feedback: ["id"]
|
|
1800
|
+
};
|
|
1801
|
+
var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
|
|
1802
|
+
var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
|
|
1803
|
+
var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
|
|
1804
|
+
var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
|
|
1805
|
+
var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
|
|
1806
|
+
var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
|
|
1807
|
+
function firstEnv(names) {
|
|
1808
|
+
for (const name of names) {
|
|
1809
|
+
const value = process.env[name];
|
|
1810
|
+
if (value)
|
|
1811
|
+
return value;
|
|
1812
|
+
}
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
function normalizeStorageMode(value) {
|
|
1816
|
+
const normalized = value?.trim().toLowerCase();
|
|
1817
|
+
if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
|
|
1818
|
+
return normalized;
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
function getStorageDatabaseUrl() {
|
|
1822
|
+
return firstEnv(STORAGE_DATABASE_ENV);
|
|
1823
|
+
}
|
|
1824
|
+
function getStorageMode() {
|
|
1825
|
+
const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
|
|
1826
|
+
if (mode)
|
|
1827
|
+
return mode;
|
|
1828
|
+
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
1829
|
+
}
|
|
1830
|
+
async function getStoragePg() {
|
|
1831
|
+
const url = getStorageDatabaseUrl();
|
|
1832
|
+
if (!url)
|
|
1833
|
+
throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
|
|
1834
|
+
return new PgAdapterAsync(url);
|
|
1835
|
+
}
|
|
1836
|
+
async function runStorageMigrations(remote) {
|
|
1837
|
+
await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
|
|
1838
|
+
for (const sql of PG_MIGRATIONS)
|
|
1839
|
+
await remote.run(sql);
|
|
1840
|
+
}
|
|
1841
|
+
async function storagePush(options) {
|
|
1842
|
+
const remote = await getStoragePg();
|
|
1843
|
+
const db = getDatabase();
|
|
1844
|
+
try {
|
|
1845
|
+
await runStorageMigrations(remote);
|
|
1846
|
+
const results = [];
|
|
1847
|
+
for (const table of resolveTables(options?.tables))
|
|
1848
|
+
results.push(await pushTable(db, remote, table));
|
|
1849
|
+
recordSyncMeta(db, "push", results);
|
|
1850
|
+
return results;
|
|
1851
|
+
} finally {
|
|
1852
|
+
await remote.close();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async function storagePull(options) {
|
|
1856
|
+
const remote = await getStoragePg();
|
|
1857
|
+
const db = getDatabase();
|
|
1858
|
+
try {
|
|
1859
|
+
await runStorageMigrations(remote);
|
|
1860
|
+
const results = [];
|
|
1861
|
+
for (const table of resolveTables(options?.tables))
|
|
1862
|
+
results.push(await pullTable(remote, db, table));
|
|
1863
|
+
recordSyncMeta(db, "pull", results);
|
|
1864
|
+
return results;
|
|
1865
|
+
} finally {
|
|
1866
|
+
await remote.close();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function storageSync(options) {
|
|
1870
|
+
const pull = await storagePull(options);
|
|
1871
|
+
const push = await storagePush(options);
|
|
1872
|
+
return { pull, push };
|
|
1873
|
+
}
|
|
1874
|
+
function getStorageSyncMetaAll() {
|
|
1875
|
+
const db = getDatabase();
|
|
1876
|
+
ensureSyncMetaTable(db);
|
|
1877
|
+
return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
|
|
1878
|
+
}
|
|
1879
|
+
function getStorageStatus() {
|
|
1880
|
+
return {
|
|
1881
|
+
configured: Boolean(getStorageDatabaseUrl()),
|
|
1882
|
+
mode: getStorageMode(),
|
|
1883
|
+
env: STORAGE_DATABASE_ENV,
|
|
1884
|
+
service: "configs",
|
|
1885
|
+
tables: STORAGE_TABLES,
|
|
1886
|
+
sync: getStorageSyncMetaAll()
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function resolveTables(tables) {
|
|
1890
|
+
if (!tables || tables.length === 0)
|
|
1891
|
+
return [...STORAGE_TABLES];
|
|
1892
|
+
const allowed = new Set(STORAGE_TABLES);
|
|
1893
|
+
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
1894
|
+
const invalid = requested.filter((table) => !allowed.has(table));
|
|
1895
|
+
if (invalid.length > 0)
|
|
1896
|
+
throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
|
|
1897
|
+
return requested;
|
|
1898
|
+
}
|
|
1899
|
+
async function pushTable(db, remote, table) {
|
|
1900
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
1901
|
+
try {
|
|
1902
|
+
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
1903
|
+
result.rowsRead = rows.length;
|
|
1904
|
+
if (rows.length === 0)
|
|
1905
|
+
return result;
|
|
1906
|
+
const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
|
|
1907
|
+
result.rowsWritten = await upsertPg(remote, table, columns, rows);
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
async function pullTable(remote, db, table) {
|
|
1914
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
1915
|
+
try {
|
|
1916
|
+
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
1917
|
+
result.rowsRead = rows.length;
|
|
1918
|
+
if (rows.length === 0)
|
|
1919
|
+
return result;
|
|
1920
|
+
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
1921
|
+
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1924
|
+
}
|
|
1925
|
+
return result;
|
|
1926
|
+
}
|
|
1927
|
+
async function filterRemoteColumns(remote, table, columns) {
|
|
1928
|
+
const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
|
|
1929
|
+
if (rows.length === 0)
|
|
1930
|
+
return columns;
|
|
1931
|
+
const allowed = new Set(rows.map((row) => row.column_name));
|
|
1932
|
+
return columns.filter((column) => allowed.has(column));
|
|
1933
|
+
}
|
|
1934
|
+
function filterLocalColumns(db, table, columns) {
|
|
1935
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
1936
|
+
const allowed = new Set(rows.map((row) => row.name));
|
|
1937
|
+
return columns.filter((column) => allowed.has(column));
|
|
1938
|
+
}
|
|
1939
|
+
async function upsertPg(remote, table, columns, rows) {
|
|
1940
|
+
if (columns.length === 0)
|
|
1941
|
+
return 0;
|
|
1942
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
1943
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
1944
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
1945
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
1946
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
1947
|
+
const fallbackKey = primaryKeys[0];
|
|
1948
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
1949
|
+
for (const row of rows) {
|
|
1950
|
+
await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
|
|
1951
|
+
}
|
|
1952
|
+
return rows.length;
|
|
1953
|
+
}
|
|
1954
|
+
function upsertSqlite(db, table, columns, rows) {
|
|
1955
|
+
if (columns.length === 0)
|
|
1956
|
+
return 0;
|
|
1957
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
1958
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
1959
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
1960
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
1961
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
1962
|
+
const fallbackKey = primaryKeys[0];
|
|
1963
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
|
|
1964
|
+
const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
|
|
1965
|
+
db.transaction((batch) => {
|
|
1966
|
+
for (const row of batch)
|
|
1967
|
+
statement.run(...columns.map((column) => coerceForSqlite(row[column])));
|
|
1968
|
+
})(rows);
|
|
1969
|
+
return rows.length;
|
|
1970
|
+
}
|
|
1971
|
+
function recordSyncMeta(db, direction, results) {
|
|
1972
|
+
ensureSyncMetaTable(db);
|
|
1973
|
+
const now2 = new Date().toISOString();
|
|
1974
|
+
const statement = db.prepare("INSERT INTO _configs_sync_meta (table_name, last_synced_at, direction) VALUES (?, ?, ?) ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at");
|
|
1975
|
+
for (const result of results) {
|
|
1976
|
+
if (result.errors.length > 0)
|
|
1977
|
+
continue;
|
|
1978
|
+
statement.run(result.table, now2, direction);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function ensureSyncMetaTable(db) {
|
|
1982
|
+
db.exec("CREATE TABLE IF NOT EXISTS _configs_sync_meta (table_name TEXT NOT NULL, last_synced_at TEXT, direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')), PRIMARY KEY (table_name, direction))");
|
|
1983
|
+
}
|
|
1984
|
+
function quoteIdent(identifier) {
|
|
1985
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1986
|
+
}
|
|
1987
|
+
function coerceForSqlite(value) {
|
|
1988
|
+
if (value === undefined || value === null)
|
|
1989
|
+
return null;
|
|
1990
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
1991
|
+
return value;
|
|
1992
|
+
if (value instanceof Date)
|
|
1993
|
+
return value.toISOString();
|
|
1994
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
1995
|
+
return value;
|
|
1996
|
+
if (typeof value === "object")
|
|
1997
|
+
return JSON.stringify(value);
|
|
1998
|
+
return String(value);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/mcp/server.ts
|
|
2002
|
+
init_configs();
|
|
2003
|
+
init_apply();
|
|
2004
|
+
init_sync_dir();
|
|
2005
|
+
|
|
2006
|
+
// src/db/profiles.ts
|
|
2007
|
+
init_types();
|
|
2008
|
+
init_database();
|
|
2009
|
+
init_configs();
|
|
2010
|
+
init_machine();
|
|
2011
|
+
function rowToProfile(row) {
|
|
2012
|
+
return {
|
|
2013
|
+
...row,
|
|
2014
|
+
selectors: JSON.parse(row.selectors || "{}"),
|
|
2015
|
+
variables: JSON.parse(row.variables || "{}")
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function getProfile(idOrSlug, db) {
|
|
2019
|
+
const d = db || getDatabase();
|
|
2020
|
+
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
2021
|
+
if (!row)
|
|
2022
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
2023
|
+
return rowToProfile(row);
|
|
2024
|
+
}
|
|
2025
|
+
function listProfiles(db) {
|
|
2026
|
+
const d = db || getDatabase();
|
|
2027
|
+
return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
|
|
2028
|
+
}
|
|
2029
|
+
function getProfileConfigs(profileIdOrSlug, db) {
|
|
2030
|
+
const d = db || getDatabase();
|
|
2031
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
2032
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
|
|
2033
|
+
if (rows.length === 0)
|
|
2034
|
+
return [];
|
|
2035
|
+
const ids = rows.map((r) => r.config_id);
|
|
2036
|
+
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
2037
|
+
}
|
|
2038
|
+
function profileHasSelectors(profile) {
|
|
2039
|
+
const selectors = profile.selectors ?? {};
|
|
2040
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
2041
|
+
}
|
|
2042
|
+
function profileMatchesMachine(profile, machine) {
|
|
2043
|
+
const selectors = profile.selectors ?? {};
|
|
2044
|
+
const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
|
|
2045
|
+
const value = candidate.trim().toLowerCase();
|
|
2046
|
+
return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
|
|
2047
|
+
});
|
|
2048
|
+
const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
|
|
2049
|
+
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
2050
|
+
return osMatches && archMatches && hostnameMatches;
|
|
2051
|
+
}
|
|
2052
|
+
function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
2053
|
+
const profiles = listProfiles(db).filter(profileHasSelectors);
|
|
2054
|
+
const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
|
|
2055
|
+
const selectors = profile.selectors;
|
|
2056
|
+
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
2057
|
+
return { profile, score };
|
|
2058
|
+
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
2059
|
+
return matches[0]?.profile ?? null;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/mcp/server.ts
|
|
2063
|
+
init_apply();
|
|
2064
|
+
init_snapshots();
|
|
2065
|
+
init_machine();
|
|
2066
|
+
|
|
2067
|
+
// src/lib/compact-output.ts
|
|
2068
|
+
var DEFAULT_LIST_LIMIT = 20;
|
|
2069
|
+
var MAX_LIST_LIMIT = 100;
|
|
2070
|
+
function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
|
|
2071
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
2072
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
2073
|
+
return fallback;
|
|
2074
|
+
return Math.min(Math.floor(parsed), max);
|
|
2075
|
+
}
|
|
2076
|
+
function parseCursor(value) {
|
|
2077
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
2078
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
2079
|
+
return 0;
|
|
2080
|
+
return Math.floor(parsed);
|
|
2081
|
+
}
|
|
2082
|
+
function paginate(items, opts = {}) {
|
|
2083
|
+
const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
|
|
2084
|
+
const cursor = parseCursor(opts.cursor);
|
|
2085
|
+
const pageItems = items.slice(cursor, cursor + limit);
|
|
2086
|
+
const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
|
|
2087
|
+
return {
|
|
2088
|
+
items: pageItems,
|
|
2089
|
+
total: items.length,
|
|
2090
|
+
limit,
|
|
2091
|
+
cursor,
|
|
2092
|
+
next_cursor: nextCursor,
|
|
2093
|
+
has_more: nextCursor !== null
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
function pagedPayload(items, opts = {}) {
|
|
2097
|
+
return {
|
|
2098
|
+
...paginate(items, opts),
|
|
2099
|
+
hint: opts.hint
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
function truncateText(value, max = 80) {
|
|
2103
|
+
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
2104
|
+
if (text.length <= max)
|
|
2105
|
+
return text;
|
|
2106
|
+
if (max <= 3)
|
|
2107
|
+
return text.slice(0, max);
|
|
2108
|
+
return `${text.slice(0, max - 3)}...`;
|
|
2109
|
+
}
|
|
2110
|
+
function summarizeConfig(config, opts = {}) {
|
|
2111
|
+
const summary = {
|
|
2112
|
+
id: config.id,
|
|
2113
|
+
slug: config.slug,
|
|
2114
|
+
name: config.name,
|
|
2115
|
+
category: config.category,
|
|
2116
|
+
agent: config.agent,
|
|
2117
|
+
kind: config.kind,
|
|
2118
|
+
format: config.format,
|
|
2119
|
+
target_path: config.target_path,
|
|
2120
|
+
output_count: config.outputs.length,
|
|
2121
|
+
version: config.version,
|
|
2122
|
+
is_template: config.is_template
|
|
2123
|
+
};
|
|
2124
|
+
if (opts.verbose) {
|
|
2125
|
+
summary.updated_at = config.updated_at;
|
|
2126
|
+
summary.description = config.description;
|
|
2127
|
+
summary.tags = config.tags;
|
|
2128
|
+
summary.outputs = config.outputs;
|
|
2129
|
+
}
|
|
2130
|
+
return summary;
|
|
2131
|
+
}
|
|
2132
|
+
function summarizeProfile(profile, opts = {}) {
|
|
2133
|
+
const selectorCount = (profile.selectors.os?.length ?? 0) + (profile.selectors.arch?.length ?? 0) + (profile.selectors.hostnames?.length ?? 0);
|
|
2134
|
+
const summary = {
|
|
2135
|
+
id: profile.id,
|
|
2136
|
+
slug: profile.slug,
|
|
2137
|
+
name: profile.name,
|
|
2138
|
+
description: opts.verbose ? profile.description : truncateText(profile.description, 80),
|
|
2139
|
+
selector_count: selectorCount,
|
|
2140
|
+
variable_count: Object.keys(profile.variables).length
|
|
2141
|
+
};
|
|
2142
|
+
if (opts.verbose) {
|
|
2143
|
+
summary.created_at = profile.created_at;
|
|
2144
|
+
summary.updated_at = profile.updated_at;
|
|
2145
|
+
summary.selectors = profile.selectors;
|
|
2146
|
+
summary.variables = profile.variables;
|
|
2147
|
+
}
|
|
2148
|
+
return summary;
|
|
2149
|
+
}
|
|
2150
|
+
function summarizeApplyResult(result) {
|
|
2151
|
+
return {
|
|
2152
|
+
config_id: result.config_id,
|
|
2153
|
+
path: result.path,
|
|
2154
|
+
dry_run: result.dry_run,
|
|
2155
|
+
changed: result.changed,
|
|
2156
|
+
agent: result.agent,
|
|
2157
|
+
transform: result.transform,
|
|
2158
|
+
output_count: result.outputs?.length ?? 0,
|
|
2159
|
+
outputs: result.outputs?.map(summarizeApplyResult)
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// src/mcp/server.ts
|
|
2164
|
+
var TOOL_DOCS = {
|
|
2165
|
+
list_configs: "List configs. Params: category?, agent?, kind?, search?, limit?, cursor?, verbose?. Defaults to a paged compact envelope without content; use get_config for full content.",
|
|
2166
|
+
get_config: "Get a config by id or slug. Returns full config including content.",
|
|
2167
|
+
create_config: "Create a new config. Required: name, content, category. Optional: agent, target_path, outputs, kind, format, tags, description, is_template.",
|
|
2168
|
+
update_config: "Update a config by id or slug. Optional: content, name, tags, description, category, agent, target_path, outputs.",
|
|
2169
|
+
apply_config: "Apply a config to its target_path on disk. Params: id_or_slug, dry_run?, verbose?. Defaults to a compact result without previous/new content.",
|
|
2170
|
+
sync_directory: "Sync a directory with the DB. Params: dir, direction ('from_disk'|'to_disk'). Returns sync result.",
|
|
2171
|
+
list_profiles: "List profiles. Params: limit?, cursor?, verbose?. Defaults to a paged compact envelope.",
|
|
2172
|
+
apply_profile: "Apply all configs in a profile to disk. Params: id_or_slug? or auto=true, dry_run?, hostname?, os?, arch?, verbose?. Defaults to compact apply results without content.",
|
|
2173
|
+
get_snapshot: "Get snapshot(s) for a config. Params: config_id_or_slug, version?. Returns latest snapshot or specific version.",
|
|
2174
|
+
get_status: "Single-call orientation. Returns: total configs, counts by category, templates, DB path.",
|
|
2175
|
+
render_template: "Render a template config with variable substitution. Params: id_or_slug, vars? (object of KEY:VALUE), use_env? (fill from env vars). Returns rendered content.",
|
|
2176
|
+
scan_secrets: "Scan configs for unredacted secrets. Params: id_or_slug? (omit for all known), fix?, limit?, cursor?. Returns paged findings without secret values.",
|
|
2177
|
+
sync_known: "Sync all known config files from disk into DB. Params: agent?, category?. Replaces sync_directory for standard use.",
|
|
2178
|
+
sync_project: "Sync project-scoped configs (CLAUDE.md, .mcp.json, AGENTS.md, rules/*.md) from a project dir. Params: project_dir (default: cwd).",
|
|
2179
|
+
search_tools: "Search tool descriptions. Params: query. Returns matching tool names and descriptions.",
|
|
2180
|
+
describe_tools: "Get full descriptions for tools. Params: names? (array). Returns tool docs."
|
|
2181
|
+
};
|
|
2182
|
+
var PROFILES = {
|
|
2183
|
+
minimal: ["get_status", "get_config", "sync_known"],
|
|
2184
|
+
standard: ["list_configs", "get_config", "create_config", "update_config", "apply_config", "sync_known", "get_status", "render_template", "scan_secrets", "list_profiles", "apply_profile", "search_tools", "describe_tools"],
|
|
2185
|
+
full: []
|
|
2186
|
+
};
|
|
2187
|
+
var activeProfile = process.env["CONFIGS_PROFILE"] || "full";
|
|
2188
|
+
var profileFilter = PROFILES[activeProfile];
|
|
2189
|
+
var ALL_LEAN_TOOLS = [
|
|
2190
|
+
{ name: "list_configs", inputSchema: { type: "object", properties: { category: { type: "string" }, agent: { type: "string" }, kind: { type: "string" }, search: { type: "string" }, limit: { type: "number" }, cursor: { type: "number" }, verbose: { type: "boolean" } } } },
|
|
2191
|
+
{ name: "get_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" } }, required: ["id_or_slug"] } },
|
|
2192
|
+
{ name: "create_config", inputSchema: { type: "object", properties: { name: { type: "string" }, content: { type: "string" }, category: { type: "string" }, agent: { type: "string" }, target_path: { type: "string" }, outputs: { type: "array", items: { type: "object" } }, kind: { type: "string" }, format: { type: "string" }, tags: { type: "array", items: { type: "string" } }, description: { type: "string" }, is_template: { type: "boolean" } }, required: ["name", "content", "category"] } },
|
|
2193
|
+
{ name: "update_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, content: { type: "string" }, name: { type: "string" }, tags: { type: "array", items: { type: "string" } }, description: { type: "string" }, category: { type: "string" }, agent: { type: "string" }, target_path: { type: "string" }, outputs: { type: "array", items: { type: "object" } } }, required: ["id_or_slug"] } },
|
|
2194
|
+
{ name: "delete_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" } }, required: ["id_or_slug"] } },
|
|
2195
|
+
{ name: "apply_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, dry_run: { type: "boolean" }, verbose: { type: "boolean" } }, required: ["id_or_slug"] } },
|
|
2196
|
+
{ name: "sync_directory", inputSchema: { type: "object", properties: { dir: { type: "string" }, direction: { type: "string" } }, required: ["dir"] } },
|
|
2197
|
+
{ name: "list_profiles", inputSchema: { type: "object", properties: { limit: { type: "number" }, cursor: { type: "number" }, verbose: { type: "boolean" } } } },
|
|
2198
|
+
{ name: "apply_profile", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, auto: { type: "boolean" }, dry_run: { type: "boolean" }, hostname: { type: "string" }, os: { type: "string" }, arch: { type: "string" }, verbose: { type: "boolean" } } } },
|
|
2199
|
+
{ name: "get_snapshot", inputSchema: { type: "object", properties: { config_id_or_slug: { type: "string" }, version: { type: "number" } }, required: ["config_id_or_slug"] } },
|
|
2200
|
+
{ name: "get_status", inputSchema: { type: "object", properties: {} } },
|
|
2201
|
+
{ name: "sync_known", inputSchema: { type: "object", properties: { agent: { type: "string" }, category: { type: "string" } } } },
|
|
2202
|
+
{ name: "sync_project", inputSchema: { type: "object", properties: { project_dir: { type: "string" } } } },
|
|
2203
|
+
{ name: "render_template", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, vars: { type: "object" }, use_env: { type: "boolean" } }, required: ["id_or_slug"] } },
|
|
2204
|
+
{ name: "scan_secrets", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, fix: { type: "boolean" }, limit: { type: "number" }, cursor: { type: "number" } } } },
|
|
2205
|
+
{ name: "search_tools", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
2206
|
+
{ name: "describe_tools", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } } } },
|
|
2207
|
+
{ name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
|
|
2208
|
+
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2209
|
+
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2210
|
+
{ name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
|
|
2211
|
+
{ name: "send_feedback", description: "Send feedback about this service", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } },
|
|
2212
|
+
{ name: "storage_status", description: "Show storage sync configuration and local sync history.", inputSchema: { type: "object", properties: {} } },
|
|
2213
|
+
{ name: "storage_push", description: "Push local configs data to storage PostgreSQL.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
|
|
2214
|
+
{ name: "storage_pull", description: "Pull configs data from storage PostgreSQL to local SQLite.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
|
|
2215
|
+
{ name: "storage_sync", description: "Bidirectional configs sync: pull then push.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } }
|
|
2216
|
+
];
|
|
2217
|
+
function ok(data) {
|
|
2218
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
2219
|
+
}
|
|
2220
|
+
function err(msg) {
|
|
2221
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: msg }) }], isError: true };
|
|
2222
|
+
}
|
|
2223
|
+
var _cfgAgents = new Map;
|
|
2224
|
+
function buildServer() {
|
|
2225
|
+
const server = new Server({ name: "configs", version: require_package().version }, { capabilities: { tools: {} } });
|
|
2226
|
+
const LEAN_TOOLS = profileFilter && profileFilter.length > 0 ? ALL_LEAN_TOOLS.filter((t) => profileFilter.includes(t.name)) : ALL_LEAN_TOOLS;
|
|
2227
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
2228
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2229
|
+
const { name, arguments: args = {} } = req.params;
|
|
2230
|
+
try {
|
|
2231
|
+
switch (name) {
|
|
2232
|
+
case "list_configs": {
|
|
2233
|
+
const configs = listConfigs({
|
|
2234
|
+
category: args["category"] || undefined,
|
|
2235
|
+
agent: args["agent"] || undefined,
|
|
2236
|
+
kind: args["kind"] || undefined,
|
|
2237
|
+
search: args["search"] || undefined
|
|
2238
|
+
});
|
|
2239
|
+
const summaries = configs.map((c) => summarizeConfig(c, { verbose: Boolean(args["verbose"]) }));
|
|
2240
|
+
return ok(pagedPayload(summaries, {
|
|
2241
|
+
limit: args["limit"],
|
|
2242
|
+
cursor: args["cursor"],
|
|
2243
|
+
hint: "Use get_config with id_or_slug for full content, or list_configs verbose=true for tags/output targets."
|
|
2244
|
+
}));
|
|
2245
|
+
}
|
|
2246
|
+
case "get_config": {
|
|
2247
|
+
const c = getConfig(args["id_or_slug"]);
|
|
2248
|
+
return ok(c);
|
|
2249
|
+
}
|
|
2250
|
+
case "create_config": {
|
|
2251
|
+
const c = createConfig({
|
|
2252
|
+
name: args["name"],
|
|
2253
|
+
content: args["content"],
|
|
2254
|
+
category: args["category"],
|
|
2255
|
+
agent: args["agent"] || undefined,
|
|
2256
|
+
target_path: args["target_path"] || undefined,
|
|
2257
|
+
outputs: args["outputs"],
|
|
2258
|
+
kind: args["kind"] || undefined,
|
|
2259
|
+
format: args["format"] || undefined,
|
|
2260
|
+
tags: args["tags"] || undefined,
|
|
2261
|
+
description: args["description"] || undefined,
|
|
2262
|
+
is_template: args["is_template"] || undefined
|
|
2263
|
+
});
|
|
2264
|
+
return ok({ id: c.id, slug: c.slug, name: c.name });
|
|
2265
|
+
}
|
|
2266
|
+
case "update_config": {
|
|
2267
|
+
const c = updateConfig(args["id_or_slug"], {
|
|
2268
|
+
content: args["content"],
|
|
2269
|
+
name: args["name"],
|
|
2270
|
+
tags: args["tags"],
|
|
2271
|
+
description: args["description"],
|
|
2272
|
+
category: args["category"],
|
|
2273
|
+
agent: args["agent"],
|
|
2274
|
+
target_path: args["target_path"],
|
|
2275
|
+
outputs: args["outputs"]
|
|
2276
|
+
});
|
|
2277
|
+
return ok({ id: c.id, slug: c.slug, version: c.version });
|
|
2278
|
+
}
|
|
2279
|
+
case "delete_config": {
|
|
2280
|
+
const { deleteConfig: deleteConfig2 } = await Promise.resolve().then(() => (init_configs(), exports_configs));
|
|
2281
|
+
deleteConfig2(args["id_or_slug"]);
|
|
2282
|
+
return ok({ deleted: true });
|
|
2283
|
+
}
|
|
2284
|
+
case "apply_config": {
|
|
2285
|
+
const config = getConfig(args["id_or_slug"]);
|
|
2286
|
+
const result = await applyConfig(config, { dryRun: args["dry_run"] });
|
|
2287
|
+
return ok(args["verbose"] ? result : summarizeApplyResult(result));
|
|
2288
|
+
}
|
|
2289
|
+
case "sync_directory": {
|
|
2290
|
+
const dir = args["dir"];
|
|
2291
|
+
const direction = args["direction"] || "from_disk";
|
|
2292
|
+
const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
|
|
2293
|
+
return ok(result);
|
|
2294
|
+
}
|
|
2295
|
+
case "list_profiles": {
|
|
2296
|
+
const profiles = listProfiles().map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
|
|
2297
|
+
return ok(pagedPayload(profiles, {
|
|
2298
|
+
limit: args["limit"],
|
|
2299
|
+
cursor: args["cursor"],
|
|
2300
|
+
hint: "Use apply_profile/get profile-specific commands for details; verbose=true includes selectors and variables."
|
|
2301
|
+
}));
|
|
2302
|
+
}
|
|
2303
|
+
case "apply_profile": {
|
|
2304
|
+
const machine = detectMachineContext({
|
|
2305
|
+
hostname: args["hostname"],
|
|
2306
|
+
os: args["os"],
|
|
2307
|
+
arch: args["arch"]
|
|
2308
|
+
});
|
|
2309
|
+
if (!args["auto"] && !args["id_or_slug"])
|
|
2310
|
+
return err("id_or_slug is required unless auto=true");
|
|
2311
|
+
const profile = args["auto"] ? resolveProfileForMachine(machine) : getProfile(args["id_or_slug"]);
|
|
2312
|
+
if (!profile)
|
|
2313
|
+
return err("No matching machine-aware profile found");
|
|
2314
|
+
const configs = getProfileConfigs(profile.id);
|
|
2315
|
+
const vars = resolveProfileVariables(profile, machine);
|
|
2316
|
+
const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars });
|
|
2317
|
+
return ok({
|
|
2318
|
+
profile: summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }),
|
|
2319
|
+
machine: {
|
|
2320
|
+
id: machine.id,
|
|
2321
|
+
os_family: machine.os_family,
|
|
2322
|
+
arch: machine.arch
|
|
2323
|
+
},
|
|
2324
|
+
results: args["verbose"] ? results : results.map(summarizeApplyResult),
|
|
2325
|
+
total: results.length,
|
|
2326
|
+
changed: results.filter((result) => result.changed).length,
|
|
2327
|
+
hint: "Set verbose=true to include previous_content/new_content in apply results."
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
case "get_snapshot": {
|
|
2331
|
+
const config = getConfig(args["config_id_or_slug"]);
|
|
2332
|
+
if (args["version"]) {
|
|
2333
|
+
const snap = getSnapshotByVersion(config.id, args["version"]);
|
|
2334
|
+
return snap ? ok(snap) : err("Snapshot not found");
|
|
2335
|
+
}
|
|
2336
|
+
const snaps = listSnapshots(config.id);
|
|
2337
|
+
return ok(snaps[0] ?? null);
|
|
2338
|
+
}
|
|
2339
|
+
case "get_status": {
|
|
2340
|
+
const stats = getConfigStats();
|
|
2341
|
+
const allConfigs = listConfigs({ kind: "file" });
|
|
2342
|
+
const { existsSync: ex, readFileSync: rf } = await import("fs");
|
|
2343
|
+
const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
|
|
2344
|
+
const { redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
|
|
2345
|
+
let drifted = 0, missing = 0, templates = 0;
|
|
2346
|
+
const driftedSlugs = [];
|
|
2347
|
+
for (const c of allConfigs) {
|
|
2348
|
+
if (c.is_template)
|
|
2349
|
+
templates++;
|
|
2350
|
+
if (!c.target_path)
|
|
2351
|
+
continue;
|
|
2352
|
+
const abs = expandPath2(c.target_path);
|
|
2353
|
+
if (!ex(abs)) {
|
|
2354
|
+
missing++;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
const disk = rf(abs, "utf-8");
|
|
2358
|
+
const { content: redactedDisk } = redactContent2(disk, c.format);
|
|
2359
|
+
if (redactedDisk !== c.content) {
|
|
2360
|
+
drifted++;
|
|
2361
|
+
driftedSlugs.push(c.slug);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return ok({
|
|
2365
|
+
total: stats["total"] || 0,
|
|
2366
|
+
by_category: Object.fromEntries(Object.entries(stats).filter(([k]) => k !== "total")),
|
|
2367
|
+
templates,
|
|
2368
|
+
drifted,
|
|
2369
|
+
drifted_configs: driftedSlugs.slice(0, 5),
|
|
2370
|
+
missing,
|
|
2371
|
+
db_path: process.env["CONFIGS_DB_PATH"] || "~/.hasna/configs/configs.db"
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
case "sync_known": {
|
|
2375
|
+
const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2376
|
+
const result = await syncKnown2({
|
|
2377
|
+
agent: args["agent"] || undefined,
|
|
2378
|
+
category: args["category"] || undefined
|
|
2379
|
+
});
|
|
2380
|
+
return ok(result);
|
|
2381
|
+
}
|
|
2382
|
+
case "sync_project": {
|
|
2383
|
+
const { syncProject: syncProject2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2384
|
+
const dir = args["project_dir"] || process.cwd();
|
|
2385
|
+
const result = await syncProject2({ projectDir: dir });
|
|
2386
|
+
return ok(result);
|
|
2387
|
+
}
|
|
2388
|
+
case "render_template": {
|
|
2389
|
+
const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
2390
|
+
const config = getConfig(args["id_or_slug"]);
|
|
2391
|
+
const vars = args["vars"] || {};
|
|
2392
|
+
if (args["use_env"]) {
|
|
2393
|
+
const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
2394
|
+
for (const v of extractTemplateVars2(config.content)) {
|
|
2395
|
+
if (!(v.name in vars) && process.env[v.name]) {
|
|
2396
|
+
vars[v.name] = process.env[v.name];
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const rendered = renderTemplate2(config.content, vars);
|
|
2401
|
+
return ok({ rendered, config_id: config.id, slug: config.slug });
|
|
2402
|
+
}
|
|
2403
|
+
case "scan_secrets": {
|
|
2404
|
+
const { scanSecrets: scanSecrets2, redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
|
|
2405
|
+
const configs = args["id_or_slug"] ? [getConfig(args["id_or_slug"])] : listConfigs({ kind: "file" });
|
|
2406
|
+
const findings = [];
|
|
2407
|
+
for (const c of configs) {
|
|
2408
|
+
const fmt = c.format;
|
|
2409
|
+
const secrets = scanSecrets2(c.content, fmt);
|
|
2410
|
+
if (secrets.length > 0) {
|
|
2411
|
+
findings.push({ slug: c.slug, secrets: secrets.length, vars: secrets.map((s) => s.varName) });
|
|
2412
|
+
if (args["fix"]) {
|
|
2413
|
+
const { content, isTemplate: isTemplate2 } = redactContent2(c.content, fmt);
|
|
2414
|
+
updateConfig(c.id, { content, is_template: isTemplate2 });
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return ok({
|
|
2419
|
+
clean: findings.length === 0,
|
|
2420
|
+
fixed: !!args["fix"],
|
|
2421
|
+
...pagedPayload(findings, {
|
|
2422
|
+
limit: args["limit"],
|
|
2423
|
+
cursor: args["cursor"],
|
|
2424
|
+
hint: "Secret values are never returned. Use id_or_slug to inspect one config, or increase limit/cursor for more findings."
|
|
2425
|
+
})
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
case "search_tools": {
|
|
2429
|
+
const query = (args["query"] || "").toLowerCase();
|
|
2430
|
+
const matches = Object.entries(TOOL_DOCS).filter(([k, v]) => k.includes(query) || v.toLowerCase().includes(query)).map(([name2, description]) => ({ name: name2, description }));
|
|
2431
|
+
return ok(matches);
|
|
2432
|
+
}
|
|
2433
|
+
case "describe_tools": {
|
|
2434
|
+
const names = args["names"];
|
|
2435
|
+
if (names) {
|
|
2436
|
+
return ok(Object.fromEntries(names.map((n) => [n, TOOL_DOCS[n] ?? "Unknown tool"])));
|
|
2437
|
+
}
|
|
2438
|
+
return ok(TOOL_DOCS);
|
|
2439
|
+
}
|
|
2440
|
+
case "register_agent": {
|
|
2441
|
+
const n = String(args["name"] ?? "");
|
|
2442
|
+
const existing = [..._cfgAgents.values()].find((x) => x.name === n);
|
|
2443
|
+
if (existing) {
|
|
2444
|
+
existing.last_seen_at = new Date().toISOString();
|
|
2445
|
+
return ok(existing);
|
|
2446
|
+
}
|
|
2447
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
2448
|
+
const ag = { id, name: n, last_seen_at: new Date().toISOString() };
|
|
2449
|
+
_cfgAgents.set(id, ag);
|
|
2450
|
+
return ok(ag);
|
|
2451
|
+
}
|
|
2452
|
+
case "heartbeat": {
|
|
2453
|
+
const ag = _cfgAgents.get(String(args["agent_id"] ?? ""));
|
|
2454
|
+
if (!ag)
|
|
2455
|
+
return err(`Agent not found: ${args["agent_id"]}`);
|
|
2456
|
+
ag.last_seen_at = new Date().toISOString();
|
|
2457
|
+
return ok({ agent_id: ag.id, name: ag.name, last_seen_at: ag.last_seen_at });
|
|
2458
|
+
}
|
|
2459
|
+
case "set_focus": {
|
|
2460
|
+
const ag = _cfgAgents.get(String(args["agent_id"] ?? ""));
|
|
2461
|
+
if (!ag)
|
|
2462
|
+
return err(`Agent not found: ${args["agent_id"]}`);
|
|
2463
|
+
ag["project_id"] = args["project_id"];
|
|
2464
|
+
return ok({ agent_id: ag.id, project_id: args["project_id"] ?? null });
|
|
2465
|
+
}
|
|
2466
|
+
case "list_agents": {
|
|
2467
|
+
return ok([..._cfgAgents.values()]);
|
|
2468
|
+
}
|
|
2469
|
+
case "send_feedback": {
|
|
2470
|
+
const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
2471
|
+
const db = getDatabase2();
|
|
2472
|
+
const pkg = require_package();
|
|
2473
|
+
db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [args["message"], args["email"] || null, args["category"] || "general", pkg.version]);
|
|
2474
|
+
return ok({ message: "Feedback saved. Thank you!" });
|
|
2475
|
+
}
|
|
2476
|
+
case "storage_status": {
|
|
2477
|
+
return ok(getStorageStatus());
|
|
2478
|
+
}
|
|
2479
|
+
case "storage_push": {
|
|
2480
|
+
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2481
|
+
return ok(await storagePush(tables ? { tables } : undefined));
|
|
2482
|
+
}
|
|
2483
|
+
case "storage_pull": {
|
|
2484
|
+
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2485
|
+
return ok(await storagePull(tables ? { tables } : undefined));
|
|
2486
|
+
}
|
|
2487
|
+
case "storage_sync": {
|
|
2488
|
+
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2489
|
+
return ok(await storageSync(tables ? { tables } : undefined));
|
|
2490
|
+
}
|
|
2491
|
+
default:
|
|
2492
|
+
return err(`Unknown tool: ${name}`);
|
|
2493
|
+
}
|
|
2494
|
+
} catch (e) {
|
|
2495
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
return server;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// src/mcp/http.ts
|
|
2502
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
2503
|
+
var MCP_HTTP_PORT = 8853;
|
|
2504
|
+
var MCP_NAME = "configs";
|
|
2505
|
+
function isStdioMode(argv) {
|
|
2506
|
+
return argv.includes("--stdio") || process.env.MCP_STDIO === "1";
|
|
2507
|
+
}
|
|
2508
|
+
function resolveHttpPort(argv) {
|
|
2509
|
+
const eqArg = argv.find((a) => a.startsWith("--port="));
|
|
2510
|
+
if (eqArg) {
|
|
2511
|
+
const parsed = Number.parseInt(eqArg.slice("--port=".length), 10);
|
|
2512
|
+
if (!Number.isNaN(parsed))
|
|
2513
|
+
return parsed;
|
|
2514
|
+
}
|
|
2515
|
+
const idx = argv.indexOf("--port");
|
|
2516
|
+
if (idx >= 0) {
|
|
2517
|
+
const parsed = Number.parseInt(argv[idx + 1] ?? "", 10);
|
|
2518
|
+
if (!Number.isNaN(parsed))
|
|
2519
|
+
return parsed;
|
|
2520
|
+
}
|
|
2521
|
+
const envPort = process.env.MCP_HTTP_PORT;
|
|
2522
|
+
if (envPort) {
|
|
2523
|
+
const parsed = Number.parseInt(envPort, 10);
|
|
2524
|
+
if (!Number.isNaN(parsed))
|
|
2525
|
+
return parsed;
|
|
2526
|
+
}
|
|
2527
|
+
return MCP_HTTP_PORT;
|
|
2528
|
+
}
|
|
2529
|
+
function healthPayload() {
|
|
2530
|
+
return { status: "ok", name: MCP_NAME };
|
|
2531
|
+
}
|
|
2532
|
+
async function handleMcpRequest(req) {
|
|
2533
|
+
const server = buildServer();
|
|
2534
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
2535
|
+
sessionIdGenerator: undefined,
|
|
2536
|
+
enableJsonResponse: true
|
|
2537
|
+
});
|
|
2538
|
+
await server.connect(transport);
|
|
2539
|
+
return transport.handleRequest(req);
|
|
2540
|
+
}
|
|
2541
|
+
async function handleMcpHttpRequest(req) {
|
|
2542
|
+
const url = new URL(req.url);
|
|
2543
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
2544
|
+
return Response.json(healthPayload());
|
|
2545
|
+
}
|
|
2546
|
+
if (url.pathname === "/mcp") {
|
|
2547
|
+
return handleMcpRequest(req);
|
|
2548
|
+
}
|
|
2549
|
+
return null;
|
|
2550
|
+
}
|
|
2551
|
+
async function startMcpHttpServer(port) {
|
|
2552
|
+
const httpServer = Bun.serve({
|
|
2553
|
+
hostname: "127.0.0.1",
|
|
2554
|
+
port,
|
|
2555
|
+
async fetch(req) {
|
|
2556
|
+
const handled = await handleMcpHttpRequest(req);
|
|
2557
|
+
if (handled)
|
|
2558
|
+
return handled;
|
|
2559
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
2562
|
+
return { port: httpServer.port, stop: () => httpServer.stop() };
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/mcp/index.ts
|
|
2566
|
+
async function main() {
|
|
2567
|
+
const argv = process.argv.slice(2);
|
|
2568
|
+
if (argv.includes("--claude")) {
|
|
2569
|
+
const proc = Bun.spawn(["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", "configs", "--", "configs-mcp"], { stdout: "inherit", stderr: "inherit" });
|
|
2570
|
+
await proc.exited;
|
|
2571
|
+
process.exit(0);
|
|
2572
|
+
}
|
|
2573
|
+
if (isStdioMode(argv)) {
|
|
2574
|
+
const server = buildServer();
|
|
2575
|
+
const transport = new StdioServerTransport;
|
|
2576
|
+
await server.connect(transport);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
const port = resolveHttpPort(argv);
|
|
2580
|
+
const { port: boundPort } = await startMcpHttpServer(port);
|
|
2581
|
+
console.error(`configs-mcp HTTP listening on http://127.0.0.1:${boundPort}/mcp`);
|
|
2582
|
+
}
|
|
2583
|
+
if (import.meta.main) {
|
|
2584
|
+
main().catch((err2) => {
|
|
2585
|
+
console.error("MCP server error:", err2);
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
export {
|
|
2590
|
+
buildServer
|
|
2591
|
+
};
|