mdkg 0.1.6 → 0.1.8
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/CHANGELOG.md +40 -0
- package/README.md +57 -3
- package/dist/cli.js +209 -3
- package/dist/commands/bundle.js +6 -0
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/db.js +560 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/format.js +10 -5
- package/dist/commands/index.js +16 -11
- package/dist/commands/init.js +3 -0
- package/dist/commands/new.js +17 -2
- package/dist/commands/subgraph.js +416 -0
- package/dist/commands/upgrade.js +43 -7
- package/dist/commands/work.js +21 -19
- package/dist/core/config.js +103 -0
- package/dist/core/project_db.js +108 -0
- package/dist/core/project_db_migrations.js +640 -0
- package/dist/core/project_db_queue.js +317 -0
- package/dist/core/project_db_snapshot.js +510 -0
- package/dist/graph/agent_file_types.js +14 -9
- package/dist/graph/node.js +2 -2
- package/dist/graph/skills_index_cache.js +1 -0
- package/dist/graph/sqlite_index.js +25 -0
- package/dist/graph/validate_graph.js +75 -63
- package/dist/init/AGENT_START.md +16 -1
- package/dist/init/CLI_COMMAND_MATRIX.md +40 -0
- package/dist/init/README.md +27 -0
- package/dist/init/config.json +11 -0
- package/dist/init/core/rule-3-cli-contract.md +7 -0
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +35 -2
- package/dist/init/init-manifest.json +7 -7
- package/dist/util/argparse.js +5 -0
- package/dist/util/refs.js +2 -2
- package/package.json +5 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enqueueProjectQueueMessage = enqueueProjectQueueMessage;
|
|
7
|
+
exports.claimProjectQueueMessage = claimProjectQueueMessage;
|
|
8
|
+
exports.ackProjectQueueMessage = ackProjectQueueMessage;
|
|
9
|
+
exports.failProjectQueueMessage = failProjectQueueMessage;
|
|
10
|
+
exports.deadLetterProjectQueueMessage = deadLetterProjectQueueMessage;
|
|
11
|
+
exports.releaseExpiredProjectQueueLeases = releaseExpiredProjectQueueLeases;
|
|
12
|
+
exports.readProjectQueueStats = readProjectQueueStats;
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
function loadDatabaseCtor() {
|
|
15
|
+
try {
|
|
16
|
+
const loaded = require("node:sqlite");
|
|
17
|
+
if (!loaded.DatabaseSync) {
|
|
18
|
+
throw new Error("node:sqlite DatabaseSync is unavailable");
|
|
19
|
+
}
|
|
20
|
+
return loaded.DatabaseSync;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
throw new Error(`node:sqlite is required for mdkg project DB queues: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function nowMs(input) {
|
|
28
|
+
if (input !== undefined) {
|
|
29
|
+
assertInteger(input, "now_ms");
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
return Date.now();
|
|
33
|
+
}
|
|
34
|
+
function assertNonEmpty(value, field) {
|
|
35
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
36
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertInteger(value, field) {
|
|
40
|
+
if (!Number.isInteger(value)) {
|
|
41
|
+
throw new Error(`${field} must be an integer`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertPositiveInteger(value, field) {
|
|
45
|
+
assertInteger(value, field);
|
|
46
|
+
if (value <= 0) {
|
|
47
|
+
throw new Error(`${field} must be greater than 0`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function stableJson(value) {
|
|
51
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
52
|
+
throw new Error("payload must be JSON-serializable");
|
|
53
|
+
}
|
|
54
|
+
if (value === null || typeof value !== "object") {
|
|
55
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
56
|
+
throw new Error("payload must be JSON-serializable");
|
|
57
|
+
}
|
|
58
|
+
const encoded = JSON.stringify(value);
|
|
59
|
+
if (encoded === undefined) {
|
|
60
|
+
throw new Error("payload must be JSON-serializable");
|
|
61
|
+
}
|
|
62
|
+
return encoded;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
66
|
+
}
|
|
67
|
+
const object = value;
|
|
68
|
+
return `{${Object.keys(object)
|
|
69
|
+
.sort()
|
|
70
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`)
|
|
71
|
+
.join(",")}}`;
|
|
72
|
+
}
|
|
73
|
+
function payloadHash(payloadJson) {
|
|
74
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(payloadJson).digest("hex")}`;
|
|
75
|
+
}
|
|
76
|
+
function toMessage(row) {
|
|
77
|
+
return {
|
|
78
|
+
queue_name: String(row.queue_name),
|
|
79
|
+
message_id: String(row.message_id),
|
|
80
|
+
dedupe_key: row.dedupe_key === null || row.dedupe_key === undefined ? null : String(row.dedupe_key),
|
|
81
|
+
payload_json: String(row.payload_json),
|
|
82
|
+
payload_hash: String(row.payload_hash),
|
|
83
|
+
status: String(row.status),
|
|
84
|
+
available_at_ms: Number(row.available_at_ms),
|
|
85
|
+
attempt_count: Number(row.attempt_count),
|
|
86
|
+
max_attempts: Number(row.max_attempts),
|
|
87
|
+
lease_owner: row.lease_owner === null || row.lease_owner === undefined ? null : String(row.lease_owner),
|
|
88
|
+
lease_deadline_ms: row.lease_deadline_ms === null || row.lease_deadline_ms === undefined ? null : Number(row.lease_deadline_ms),
|
|
89
|
+
created_at_ms: Number(row.created_at_ms),
|
|
90
|
+
updated_at_ms: Number(row.updated_at_ms),
|
|
91
|
+
last_error: row.last_error === null || row.last_error === undefined ? null : String(row.last_error),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function getMessage(db, queueName, messageId) {
|
|
95
|
+
const row = db
|
|
96
|
+
.prepare("SELECT * FROM project_queue_message WHERE queue_name = ? AND message_id = ?")
|
|
97
|
+
.get(queueName, messageId);
|
|
98
|
+
return row ? toMessage(row) : null;
|
|
99
|
+
}
|
|
100
|
+
function withDb(databasePath, fn) {
|
|
101
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
102
|
+
const db = new DatabaseSync(databasePath);
|
|
103
|
+
try {
|
|
104
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
105
|
+
return fn(db);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function withImmediateTransaction(db, fn) {
|
|
112
|
+
db.exec("BEGIN IMMEDIATE");
|
|
113
|
+
try {
|
|
114
|
+
const result = fn();
|
|
115
|
+
db.exec("COMMIT");
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
try {
|
|
120
|
+
db.exec("ROLLBACK");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// ignore rollback failures when no transaction is active
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function enqueueProjectQueueMessage(databasePath, input) {
|
|
129
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
130
|
+
assertNonEmpty(input.message_id, "message_id");
|
|
131
|
+
if (input.dedupe_key !== undefined && input.dedupe_key !== null) {
|
|
132
|
+
assertNonEmpty(input.dedupe_key, "dedupe_key");
|
|
133
|
+
}
|
|
134
|
+
const currentNow = nowMs(input.now_ms);
|
|
135
|
+
const availableAt = input.available_at_ms ?? currentNow;
|
|
136
|
+
assertInteger(availableAt, "available_at_ms");
|
|
137
|
+
const maxAttempts = input.max_attempts ?? 3;
|
|
138
|
+
assertPositiveInteger(maxAttempts, "max_attempts");
|
|
139
|
+
const payloadJson = stableJson(input.payload);
|
|
140
|
+
const hash = payloadHash(payloadJson);
|
|
141
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
142
|
+
if (input.dedupe_key) {
|
|
143
|
+
const duplicate = db
|
|
144
|
+
.prepare("SELECT * FROM project_queue_message WHERE queue_name = ? AND dedupe_key = ?")
|
|
145
|
+
.get(input.queue_name, input.dedupe_key);
|
|
146
|
+
if (duplicate) {
|
|
147
|
+
return { created: false, duplicate: true, message: toMessage(duplicate) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
db
|
|
151
|
+
.prepare([
|
|
152
|
+
"INSERT INTO project_queue_message",
|
|
153
|
+
"(queue_name, message_id, dedupe_key, payload_json, payload_hash, status, available_at_ms, attempt_count, max_attempts, lease_owner, lease_deadline_ms, created_at_ms, updated_at_ms, last_error)",
|
|
154
|
+
"VALUES (?, ?, ?, ?, ?, 'ready', ?, 0, ?, NULL, NULL, ?, ?, NULL)",
|
|
155
|
+
].join(" "))
|
|
156
|
+
.run(input.queue_name, input.message_id, input.dedupe_key ?? null, payloadJson, hash, availableAt, maxAttempts, currentNow, currentNow);
|
|
157
|
+
const message = getMessage(db, input.queue_name, input.message_id);
|
|
158
|
+
if (!message) {
|
|
159
|
+
throw new Error("queued message could not be reloaded after insert");
|
|
160
|
+
}
|
|
161
|
+
return { created: true, duplicate: false, message };
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
function claimProjectQueueMessage(databasePath, input) {
|
|
165
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
166
|
+
assertNonEmpty(input.lease_owner, "lease_owner");
|
|
167
|
+
assertPositiveInteger(input.lease_ms, "lease_ms");
|
|
168
|
+
const currentNow = nowMs(input.now_ms);
|
|
169
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
170
|
+
const row = db
|
|
171
|
+
.prepare([
|
|
172
|
+
"SELECT * FROM project_queue_message",
|
|
173
|
+
"WHERE queue_name = ?",
|
|
174
|
+
"AND (",
|
|
175
|
+
" (status = 'ready' AND available_at_ms <= ?)",
|
|
176
|
+
" OR (status = 'leased' AND lease_deadline_ms <= ?)",
|
|
177
|
+
")",
|
|
178
|
+
"ORDER BY available_at_ms ASC, created_at_ms ASC, message_id ASC",
|
|
179
|
+
"LIMIT 1",
|
|
180
|
+
].join(" "))
|
|
181
|
+
.get(input.queue_name, currentNow, currentNow);
|
|
182
|
+
if (!row) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const message = toMessage(row);
|
|
186
|
+
db
|
|
187
|
+
.prepare("UPDATE project_queue_message SET status = 'leased', lease_owner = ?, lease_deadline_ms = ?, updated_at_ms = ? WHERE queue_name = ? AND message_id = ?")
|
|
188
|
+
.run(input.lease_owner, currentNow + input.lease_ms, currentNow, input.queue_name, message.message_id);
|
|
189
|
+
return getMessage(db, input.queue_name, message.message_id);
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
function requireLeasedMessage(db, queueName, messageId, leaseOwner) {
|
|
193
|
+
const message = getMessage(db, queueName, messageId);
|
|
194
|
+
if (!message) {
|
|
195
|
+
throw new Error(`queue message not found: ${queueName}/${messageId}`);
|
|
196
|
+
}
|
|
197
|
+
if (message.status !== "leased" || message.lease_owner !== leaseOwner) {
|
|
198
|
+
throw new Error(`queue message ${messageId} is not leased by ${leaseOwner}`);
|
|
199
|
+
}
|
|
200
|
+
return message;
|
|
201
|
+
}
|
|
202
|
+
function ackProjectQueueMessage(databasePath, input) {
|
|
203
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
204
|
+
assertNonEmpty(input.message_id, "message_id");
|
|
205
|
+
assertNonEmpty(input.lease_owner, "lease_owner");
|
|
206
|
+
const currentNow = nowMs(input.now_ms);
|
|
207
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
208
|
+
requireLeasedMessage(db, input.queue_name, input.message_id, input.lease_owner);
|
|
209
|
+
db
|
|
210
|
+
.prepare("UPDATE project_queue_message SET status = 'acked', lease_owner = NULL, lease_deadline_ms = NULL, updated_at_ms = ? WHERE queue_name = ? AND message_id = ?")
|
|
211
|
+
.run(currentNow, input.queue_name, input.message_id);
|
|
212
|
+
const message = getMessage(db, input.queue_name, input.message_id);
|
|
213
|
+
if (!message) {
|
|
214
|
+
throw new Error("acked message could not be reloaded");
|
|
215
|
+
}
|
|
216
|
+
return message;
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
function failProjectQueueMessage(databasePath, input) {
|
|
220
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
221
|
+
assertNonEmpty(input.message_id, "message_id");
|
|
222
|
+
assertNonEmpty(input.lease_owner, "lease_owner");
|
|
223
|
+
assertNonEmpty(input.error, "error");
|
|
224
|
+
const currentNow = nowMs(input.now_ms);
|
|
225
|
+
const retryAfter = input.retry_after_ms ?? 0;
|
|
226
|
+
assertInteger(retryAfter, "retry_after_ms");
|
|
227
|
+
if (retryAfter < 0) {
|
|
228
|
+
throw new Error("retry_after_ms must be greater than or equal to 0");
|
|
229
|
+
}
|
|
230
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
231
|
+
const message = requireLeasedMessage(db, input.queue_name, input.message_id, input.lease_owner);
|
|
232
|
+
const attemptCount = message.attempt_count + 1;
|
|
233
|
+
const status = attemptCount >= message.max_attempts ? "dead_letter" : "ready";
|
|
234
|
+
const availableAt = status === "ready" ? currentNow + retryAfter : currentNow;
|
|
235
|
+
db
|
|
236
|
+
.prepare([
|
|
237
|
+
"UPDATE project_queue_message",
|
|
238
|
+
"SET status = ?, attempt_count = ?, available_at_ms = ?, lease_owner = NULL, lease_deadline_ms = NULL, updated_at_ms = ?, last_error = ?",
|
|
239
|
+
"WHERE queue_name = ? AND message_id = ?",
|
|
240
|
+
].join(" "))
|
|
241
|
+
.run(status, attemptCount, availableAt, currentNow, input.error, input.queue_name, input.message_id);
|
|
242
|
+
const updated = getMessage(db, input.queue_name, input.message_id);
|
|
243
|
+
if (!updated) {
|
|
244
|
+
throw new Error("failed message could not be reloaded");
|
|
245
|
+
}
|
|
246
|
+
return updated;
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
function deadLetterProjectQueueMessage(databasePath, input) {
|
|
250
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
251
|
+
assertNonEmpty(input.message_id, "message_id");
|
|
252
|
+
assertNonEmpty(input.lease_owner, "lease_owner");
|
|
253
|
+
assertNonEmpty(input.error, "error");
|
|
254
|
+
const currentNow = nowMs(input.now_ms);
|
|
255
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
256
|
+
requireLeasedMessage(db, input.queue_name, input.message_id, input.lease_owner);
|
|
257
|
+
db
|
|
258
|
+
.prepare("UPDATE project_queue_message SET status = 'dead_letter', lease_owner = NULL, lease_deadline_ms = NULL, updated_at_ms = ?, last_error = ? WHERE queue_name = ? AND message_id = ?")
|
|
259
|
+
.run(currentNow, input.error, input.queue_name, input.message_id);
|
|
260
|
+
const message = getMessage(db, input.queue_name, input.message_id);
|
|
261
|
+
if (!message) {
|
|
262
|
+
throw new Error("dead-lettered message could not be reloaded");
|
|
263
|
+
}
|
|
264
|
+
return message;
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
function releaseExpiredProjectQueueLeases(databasePath, input = {}) {
|
|
268
|
+
const currentNow = nowMs(input.now_ms);
|
|
269
|
+
if (input.queue_name !== undefined) {
|
|
270
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
271
|
+
}
|
|
272
|
+
return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
|
|
273
|
+
const result = input.queue_name
|
|
274
|
+
? db
|
|
275
|
+
.prepare("UPDATE project_queue_message SET status = 'ready', lease_owner = NULL, lease_deadline_ms = NULL, updated_at_ms = ? WHERE queue_name = ? AND status = 'leased' AND lease_deadline_ms <= ?")
|
|
276
|
+
.run(currentNow, input.queue_name, currentNow)
|
|
277
|
+
: db
|
|
278
|
+
.prepare("UPDATE project_queue_message SET status = 'ready', lease_owner = NULL, lease_deadline_ms = NULL, updated_at_ms = ? WHERE status = 'leased' AND lease_deadline_ms <= ?")
|
|
279
|
+
.run(currentNow, currentNow);
|
|
280
|
+
return { released_count: Number(result.changes ?? 0) };
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
function readProjectQueueStats(databasePath, input = {}) {
|
|
284
|
+
if (input.queue_name !== undefined) {
|
|
285
|
+
assertNonEmpty(input.queue_name, "queue_name");
|
|
286
|
+
}
|
|
287
|
+
const currentNow = nowMs(input.now_ms);
|
|
288
|
+
return withDb(databasePath, (db) => {
|
|
289
|
+
const params = input.queue_name ? [input.queue_name] : [];
|
|
290
|
+
const where = input.queue_name ? "WHERE queue_name = ?" : "";
|
|
291
|
+
const rows = db
|
|
292
|
+
.prepare(`SELECT status, COUNT(*) AS count FROM project_queue_message ${where} GROUP BY status`)
|
|
293
|
+
.all(...params);
|
|
294
|
+
const byStatus = {
|
|
295
|
+
ready: 0,
|
|
296
|
+
leased: 0,
|
|
297
|
+
acked: 0,
|
|
298
|
+
dead_letter: 0,
|
|
299
|
+
};
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
byStatus[String(row.status)] = Number(row.count);
|
|
302
|
+
}
|
|
303
|
+
const total = Object.values(byStatus).reduce((sum, count) => sum + count, 0);
|
|
304
|
+
const readyAvailable = db
|
|
305
|
+
.prepare(`SELECT COUNT(*) AS count FROM project_queue_message ${where} ${where ? "AND" : "WHERE"} status = 'ready' AND available_at_ms <= ?`)
|
|
306
|
+
.get(...params, currentNow);
|
|
307
|
+
const leasedExpired = db
|
|
308
|
+
.prepare(`SELECT COUNT(*) AS count FROM project_queue_message ${where} ${where ? "AND" : "WHERE"} status = 'leased' AND lease_deadline_ms <= ?`)
|
|
309
|
+
.get(...params, currentNow);
|
|
310
|
+
return {
|
|
311
|
+
total,
|
|
312
|
+
by_status: byStatus,
|
|
313
|
+
ready_available: Number(readyAvailable?.count ?? 0),
|
|
314
|
+
leased_expired: Number(leasedExpired?.count ?? 0),
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
}
|