herdr-plugin-amq 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/blobs.mjs +82 -4
- package/src/store.mjs +7 -4
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
package/src/blobs.mjs
CHANGED
|
@@ -161,10 +161,57 @@ export function getBlob(sha256, amqRoot) {
|
|
|
161
161
|
|
|
162
162
|
const gitRefCache = new Map();
|
|
163
163
|
const headCommitCache = new Map();
|
|
164
|
+
const timestampCommitCache = new Map();
|
|
164
165
|
|
|
165
166
|
export function clearGitRefCache() {
|
|
166
167
|
gitRefCache.clear();
|
|
167
168
|
headCommitCache.clear();
|
|
169
|
+
timestampCommitCache.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the git commit active before or at a given ISO timestamp.
|
|
174
|
+
*/
|
|
175
|
+
export function getCommitAtTimestamp(repoRoot, timestamp) {
|
|
176
|
+
if (!repoRoot || !timestamp) return null;
|
|
177
|
+
const iso = typeof timestamp === "string" ? timestamp.trim() : new Date(timestamp).toISOString();
|
|
178
|
+
const cacheKey = `${repoRoot}:${iso}`;
|
|
179
|
+
if (timestampCommitCache.has(cacheKey)) {
|
|
180
|
+
return timestampCommitCache.get(cacheKey);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const commit = execFileSync("git", ["rev-list", "-n", "1", `--before=${iso}`, "HEAD"], {
|
|
185
|
+
cwd: repoRoot,
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
188
|
+
}).trim();
|
|
189
|
+
|
|
190
|
+
const result = commit && /^[a-f0-9]{7,40}$/i.test(commit) ? commit : null;
|
|
191
|
+
timestampCommitCache.set(cacheKey, result);
|
|
192
|
+
return result;
|
|
193
|
+
} catch {
|
|
194
|
+
timestampCommitCache.set(cacheKey, null);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract commit hashes explicitly mentioned in message text.
|
|
201
|
+
*/
|
|
202
|
+
export function extractMentionedCommits(text = "") {
|
|
203
|
+
if (!text || typeof text !== "string") return [];
|
|
204
|
+
const commits = [];
|
|
205
|
+
const regex = /\b(?:commit|commitado|commitada|fixado|merge|sha|rev)?[:\s*`"'(]*([0-9a-f]{7,40})\b/gi;
|
|
206
|
+
let match;
|
|
207
|
+
while ((match = regex.exec(text)) !== null) {
|
|
208
|
+
const hex = match[1].toLowerCase();
|
|
209
|
+
if (/^[0-9]+$/.test(hex) && hex.length < 40) continue;
|
|
210
|
+
if (!commits.includes(hex)) {
|
|
211
|
+
commits.push(hex);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return commits;
|
|
168
215
|
}
|
|
169
216
|
|
|
170
217
|
/**
|
|
@@ -290,7 +337,7 @@ export function readGitRef(repoRoot, commitSha, relativePath) {
|
|
|
290
337
|
* - Committed files in repoRoot can be pinned to git HEAD.
|
|
291
338
|
* - Already pinned/stored items retain their immutable URLs.
|
|
292
339
|
*/
|
|
293
|
-
export function ingestAttachment(candidate, amqRoot, repoRoot) {
|
|
340
|
+
export function ingestAttachment(candidate, amqRoot, repoRoot, { timestamp = null, text = "" } = {}) {
|
|
294
341
|
if (!candidate) return null;
|
|
295
342
|
|
|
296
343
|
// Already a structured descriptor
|
|
@@ -316,7 +363,7 @@ export function ingestAttachment(candidate, amqRoot, repoRoot) {
|
|
|
316
363
|
}
|
|
317
364
|
|
|
318
365
|
if (candidate.path) {
|
|
319
|
-
return ingestAttachment(candidate.path, amqRoot, repoRoot);
|
|
366
|
+
return ingestAttachment(candidate.path, amqRoot, repoRoot, { timestamp, text });
|
|
320
367
|
}
|
|
321
368
|
}
|
|
322
369
|
|
|
@@ -363,16 +410,47 @@ export function ingestAttachment(candidate, amqRoot, repoRoot) {
|
|
|
363
410
|
return storeBlob(diskPath, amqRoot, path.basename(diskPath));
|
|
364
411
|
}
|
|
365
412
|
|
|
366
|
-
// If it's inside repoRoot and git is present,
|
|
413
|
+
// If it's inside repoRoot and git is present, pin to commit at timestamp (or HEAD)
|
|
367
414
|
if (repoRoot && diskPath.startsWith(repoRoot)) {
|
|
368
415
|
const rel = path.relative(repoRoot, diskPath);
|
|
369
|
-
const
|
|
416
|
+
const commit = (timestamp ? getCommitAtTimestamp(repoRoot, timestamp) : null) || "HEAD";
|
|
417
|
+
const gitRef = pinGitRef(repoRoot, rel, commit);
|
|
370
418
|
if (gitRef) return gitRef;
|
|
371
419
|
}
|
|
372
420
|
|
|
373
421
|
// Default fallback for existing files: store into blobstore for guarantee
|
|
374
422
|
return storeBlob(diskPath, amqRoot, path.basename(diskPath));
|
|
375
423
|
}
|
|
424
|
+
|
|
425
|
+
// ─── Historical Git Pinning Fallback ───
|
|
426
|
+
// If the file does not exist on disk right now (e.g. deleted or renamed in subsequent commits),
|
|
427
|
+
// check if it existed in Git at the time the message was created or at a commit mentioned in the message!
|
|
428
|
+
if (!diskPath && repoRoot) {
|
|
429
|
+
const cleanRel = raw.startsWith(repoRoot) ? path.relative(repoRoot, raw) : raw.replace(/^[/\\]+/, "");
|
|
430
|
+
if (!cleanRel.includes("..") && !path.isAbsolute(cleanRel)) {
|
|
431
|
+
const candidateCommits = [];
|
|
432
|
+
if (text) {
|
|
433
|
+
candidateCommits.push(...extractMentionedCommits(text));
|
|
434
|
+
}
|
|
435
|
+
if (timestamp) {
|
|
436
|
+
const atTime = getCommitAtTimestamp(repoRoot, timestamp);
|
|
437
|
+
if (atTime && !candidateCommits.includes(atTime)) {
|
|
438
|
+
candidateCommits.push(atTime);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
candidateCommits.push("HEAD");
|
|
442
|
+
|
|
443
|
+
for (const commitSha of candidateCommits) {
|
|
444
|
+
const gitRef = pinGitRef(repoRoot, cleanRel, commitSha);
|
|
445
|
+
if (gitRef && gitRef.exists) {
|
|
446
|
+
return {
|
|
447
|
+
...gitRef,
|
|
448
|
+
pinnedAt: timestamp || null,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
376
454
|
}
|
|
377
455
|
|
|
378
456
|
return null;
|
package/src/store.mjs
CHANGED
|
@@ -163,7 +163,7 @@ export function resolveAttachmentPath(ref, amqRoot) {
|
|
|
163
163
|
* Extract attachments and referenced images/logs from body or frontmatter,
|
|
164
164
|
* verifying presence on disk and providing existence flags.
|
|
165
165
|
*/
|
|
166
|
-
export function extractAttachments(body = "", metaAttachments = [], amqRoot = null) {
|
|
166
|
+
export function extractAttachments(body = "", metaAttachments = [], amqRoot = null, meta = {}) {
|
|
167
167
|
const attachments = [];
|
|
168
168
|
const seen = new Set();
|
|
169
169
|
const repoRoot = getRepoRootFromAmq(amqRoot);
|
|
@@ -184,10 +184,13 @@ export function extractAttachments(body = "", metaAttachments = [], amqRoot = nu
|
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// 1. Try hybrid CAS ingestion / Git pinning (Option A + B)
|
|
187
|
+
// 1. Try hybrid CAS ingestion / Git pinning (Option A + B) with timestamp
|
|
188
188
|
if (amqRoot) {
|
|
189
189
|
try {
|
|
190
|
-
const ingested = ingestAttachment(rawRef, amqRoot, repoRoot
|
|
190
|
+
const ingested = ingestAttachment(rawRef, amqRoot, repoRoot, {
|
|
191
|
+
timestamp: meta?.created || null,
|
|
192
|
+
text: body,
|
|
193
|
+
});
|
|
191
194
|
if (ingested && ingested.exists) {
|
|
192
195
|
attachments.push({
|
|
193
196
|
...ingested,
|
|
@@ -394,7 +397,7 @@ export function parseMessageFile(filePath, amqRoot = null) {
|
|
|
394
397
|
if (isOutbox) folder = "sent";
|
|
395
398
|
|
|
396
399
|
const root = amqRoot || (filePath.includes("/agents/") ? filePath.split(path.sep + "agents" + path.sep)[0] : null);
|
|
397
|
-
const attachments = extractAttachments(body, meta.attachments, root);
|
|
400
|
+
const attachments = extractAttachments(body, meta.attachments, root, meta);
|
|
398
401
|
const hasImage = attachments.some((a) => a.isImage);
|
|
399
402
|
const hasAttachment = attachments.length > 0;
|
|
400
403
|
|