mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
389 lines
13 KiB
TypeScript
389 lines
13 KiB
TypeScript
/**
|
|
* Copyright 2026 Reto Meier
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { applyPatch, createTwoFilesPatch, parsePatch } from 'diff';
|
|
import { MultiAgentToolContext } from '../momoa_core/types.js';
|
|
import { Buffer } from 'node:buffer';
|
|
import * as zlib from 'node:zlib';
|
|
|
|
const LOCK_FILE_NAMES = new Set([
|
|
'package-lock.json',
|
|
'yarn.lock',
|
|
'pnpm-lock.yaml',
|
|
'Gemfile.lock',
|
|
'Pipfile.lock',
|
|
'poetry.lock',
|
|
'composer.lock',
|
|
'mix.lock',
|
|
'pubspec.lock',
|
|
'go.sum',
|
|
'Cargo.lock',
|
|
'npm-shrinkwrap.json'
|
|
]);
|
|
|
|
export function isLockFile(filename: string): boolean {
|
|
// Check standard exact matches
|
|
if (LOCK_FILE_NAMES.has(filename)) return true;
|
|
// Check if it's a lock file in a subfolder (e.g., backend/go.sum)
|
|
const baseName = filename.split(/[/\\]/).pop();
|
|
return baseName ? LOCK_FILE_NAMES.has(baseName) : false;
|
|
}
|
|
|
|
export function getLockFileHiddenPlaceholder(filename: string): string {
|
|
return `--- Content of machine-generated lock file '${filename}' hidden to save context ---`;
|
|
}
|
|
|
|
export function getLockFileFileDescription(): string {
|
|
return 'Machine-generated lock file.'
|
|
}
|
|
|
|
/**
|
|
* Describes the changes applied by applyDiff.
|
|
*/
|
|
export interface ApplyDiffChanges {
|
|
created: string[];
|
|
deleted: string[];
|
|
modified: string[];
|
|
renamed: { from: string; to: string }[];
|
|
}
|
|
|
|
/**
|
|
* Generates a unified diff string for all created, deleted, or modified files.
|
|
* @param originalFileMap The map of filenames to their original content.
|
|
* @param finalFileMap The map of filenames to their final content.
|
|
* @param editedFilesSet The set of filenames that have been touched.
|
|
* @param binaryFiles Set of all binary files (original and final).
|
|
* @param redactFiles If true, replaces lock and binary file diff bodies with placeholders.
|
|
* @returns A string in the unified diff format.
|
|
*/
|
|
export function generateDiff(
|
|
originalFileMap: Map<string, string>,
|
|
finalFileMap: Map<string, string>,
|
|
editedFilesSet: Set<string>,
|
|
binaryFiles: Set<string>,
|
|
redactFiles: boolean = false
|
|
): string {
|
|
let fullDiff = '';
|
|
|
|
// Sort filenames for deterministic diff output
|
|
const sortedFiles = Array.from(editedFilesSet).sort();
|
|
|
|
for (const filename of sortedFiles) {
|
|
const originalExists = originalFileMap.has(filename);
|
|
const finalExists = finalFileMap.has(filename);
|
|
|
|
const originalContent = originalFileMap.get(filename) ?? '';
|
|
const finalContent = finalFileMap.get(filename) ?? '';
|
|
|
|
if (originalContent === finalContent && originalExists === finalExists) {
|
|
continue;
|
|
}
|
|
|
|
const isDeleted = originalExists && !finalExists;
|
|
const isCreation = !originalExists && finalExists;
|
|
const isBinary = binaryFiles.has(filename);
|
|
const isLock = isLockFile(filename);
|
|
|
|
// --- Standard Git Header ---
|
|
fullDiff += `diff --git a/${filename} b/${filename}\n`;
|
|
|
|
if (isCreation) {
|
|
fullDiff += `new file mode 100644\n`;
|
|
}
|
|
if (isDeleted) {
|
|
fullDiff += `deleted file mode 100644\n`;
|
|
}
|
|
|
|
// --- Binary File Handling ---
|
|
if (isBinary && !isDeleted) {
|
|
if (redactFiles) {
|
|
// Redacted mode: fake a regular patch with a placeholder message
|
|
fullDiff += isCreation ? `--- /dev/null\n` : `--- a/${filename}\n`;
|
|
fullDiff += `+++ b/${filename}\n`;
|
|
fullDiff += `@@ -0,0 +1 @@\n+ [Binary file changes hidden to save context]\n`;
|
|
} else {
|
|
// Full mode: standard git binary message
|
|
// Note: standard git output for binary often omits ---/+++ unless --text is forced,
|
|
// but usually just says "Binary files ... differ"
|
|
fullDiff += `Binary files a/${filename} and b/${filename} differ\n`;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Lock File Redaction ---
|
|
if (redactFiles && isLock) {
|
|
if (isDeleted) {
|
|
fullDiff += `--- a/${filename}\n+++ /dev/null\n`;
|
|
fullDiff += `@@ -1 +0,0 @@\n- [Machine-generated lock file deleted]\n`;
|
|
} else {
|
|
fullDiff += isCreation ? `--- /dev/null\n` : `--- a/${filename}\n`;
|
|
fullDiff += `+++ b/${filename}\n`;
|
|
fullDiff += `@@ -0,0 +1 @@\n+ [Machine-generated lock file changes hidden to save context]\n`;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Standard Text File Diffing ---
|
|
const patch = createTwoFilesPatch(
|
|
isCreation ? '/dev/null' : `a/${filename}`,
|
|
isDeleted ? '/dev/null' : `b/${filename}`,
|
|
originalContent,
|
|
finalContent,
|
|
'',
|
|
'',
|
|
{ context: 3 }
|
|
);
|
|
|
|
const cleanPatch = patch
|
|
.split('\n')
|
|
.filter(line => !line.startsWith('==================================================================='))
|
|
.join('\n');
|
|
|
|
// If the file is a deleted binary (that wasn't caught above because isBinary && !isDeleted was false),
|
|
// we might get a weird patch if we aren't careful, but usually binaryFiles check handles it.
|
|
// Assuming standard text flow here:
|
|
fullDiff += cleanPatch
|
|
}
|
|
|
|
return fullDiff;
|
|
}
|
|
|
|
/**
|
|
* Applies a unified diff string to an in-memory file map and updates the set of edited files.
|
|
* @param fileMap The map of filenames to content that the diff will be applied to.
|
|
* @param editedFilesSet The set of edited filenames to update.
|
|
* @param diffString The unified diff string containing patches for one or more files.
|
|
* @returns An object indicating success, a list of changed files, and an optional error message.
|
|
*/
|
|
export function applyDiff(
|
|
fileMap: Map<string, string>,
|
|
diffString: string
|
|
): { success: boolean; changes?: ApplyDiffChanges; error?: string } {
|
|
try {
|
|
// parsePatch is generally resilient to extra git headers we added above.
|
|
const patches = parsePatch(diffString);
|
|
const changes: ApplyDiffChanges = {
|
|
created: [],
|
|
deleted: [],
|
|
modified: [],
|
|
renamed: [],
|
|
};
|
|
|
|
for (const patch of patches) {
|
|
const isDeletion = patch.newFileName === '/dev/null';
|
|
const isCreation = patch.oldFileName === '/dev/null';
|
|
|
|
// For deletions, the relevant filename is the old one.
|
|
// For creations/modifications/renames, it's the new one.
|
|
const filename = (isDeletion
|
|
? patch.oldFileName?.replace(/^a\//, '')
|
|
: patch.newFileName?.replace(/^b\//, '')) as string;
|
|
|
|
if (!filename) {
|
|
return { success: false, error: 'Could not determine filename from a patch.' };
|
|
}
|
|
|
|
// Handle file deletions
|
|
if (isDeletion) {
|
|
if (fileMap.has(filename)) {
|
|
fileMap.delete(filename);
|
|
changes.deleted.push(filename);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle file creations
|
|
if (isCreation) {
|
|
const newContent = applyPatch('', patch, { fuzzFactor: 4 });
|
|
if (newContent === false) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to apply creation patch to '${filename}'.`,
|
|
};
|
|
}
|
|
fileMap.set(filename, newContent);
|
|
changes.created.push(filename);
|
|
continue;
|
|
}
|
|
|
|
// Handle file modification OR rename
|
|
const oldFilename = patch.oldFileName?.replace(/^a\//, '');
|
|
if (!oldFilename) {
|
|
return { success: false, error: 'Could not determine old filename from modification patch.' };
|
|
}
|
|
|
|
let originalContent = fileMap.get(oldFilename);
|
|
let isImplicitCreation = false;
|
|
|
|
if (originalContent === undefined) {
|
|
// Fallback if we don't have the original file in memory but a patch references it.
|
|
// This might happen in partial context scenarios.
|
|
originalContent = '';
|
|
isImplicitCreation = true;
|
|
}
|
|
|
|
const newContent = applyPatch(originalContent, patch, { fuzzFactor: 4 });
|
|
|
|
if (newContent === false) {
|
|
const errorContext = isImplicitCreation ? 'implicit creation' : 'patch';
|
|
return {
|
|
success: false,
|
|
error: `Failed to apply ${errorContext} to '${oldFilename}'.`,
|
|
};
|
|
}
|
|
|
|
// Set the new content under the new filename
|
|
fileMap.set(filename, newContent);
|
|
|
|
if (oldFilename !== filename) {
|
|
// This was a rename
|
|
fileMap.delete(oldFilename);
|
|
changes.renamed.push({ from: oldFilename, to: filename });
|
|
} else {
|
|
// This was a simple modification
|
|
changes.modified.push(filename);
|
|
}
|
|
}
|
|
|
|
return { success: true, changes: changes };
|
|
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'An unknown error occurred while applying the diff.';
|
|
return { success: false, error: message };
|
|
}
|
|
}
|
|
|
|
export function generateDiffString(context: MultiAgentToolContext, redactFiles: boolean = false): string {
|
|
const binaryPlaceholder = "[binary file content]";
|
|
const combinedOriginalFileMap = new Map<string, string>([
|
|
...(context.originalFileMap || new Map()),
|
|
...Array.from(context.originalBinaryFileMap?.keys() || []).map(key => [key, binaryPlaceholder] as [string, string])
|
|
]);
|
|
const combinedFinalFileMap = new Map<string, string>([
|
|
...context.fileMap,
|
|
...Array.from(context.binaryFileMap.keys()).map(key => [key, binaryPlaceholder] as [string, string])
|
|
]);
|
|
|
|
const allBinaryFiles = new Set([
|
|
...(context.originalBinaryFileMap?.keys() || []),
|
|
...context.binaryFileMap.keys()
|
|
]);
|
|
|
|
let newDiffString = generateDiff(
|
|
combinedOriginalFileMap,
|
|
combinedFinalFileMap,
|
|
context.editedFilesSet,
|
|
allBinaryFiles,
|
|
redactFiles
|
|
);
|
|
|
|
if (!newDiffString) {
|
|
newDiffString = "---No changes detected. All files are in their original state.---";
|
|
}
|
|
|
|
let header = `#Unified Diff for Project\nThe following diff is a live source of truth for the changes made to the project's files up to this point.`;
|
|
if (redactFiles) {
|
|
header += `\nNote: Machine-generated lock files and binary files may be redacted to save context window space.`;
|
|
}
|
|
|
|
const newDiffBlock = `${header}\n---Start of Unified Diff---\n${newDiffString}\n---End of Unified Diff---`;
|
|
return newDiffBlock;
|
|
}
|
|
|
|
/**
|
|
* Decodes a Git binary patch string into a raw Buffer.
|
|
* Handles the Git-specific Base85 encoding and zlib inflation.
|
|
*/
|
|
export function decodeGitBinaryPatch(patchText: string): Buffer | null {
|
|
// 1. Extract the literal block.
|
|
// Git binary patches usually contain two 'literal' blocks (new vs old).
|
|
// We generally want the first one (the new content).
|
|
const lines = patchText.split('\n');
|
|
const dataLines: string[] = [];
|
|
let inLiteral = false;
|
|
|
|
for (let line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith('literal')) {
|
|
// If we already found a block and hit a new literal, we stop (we only want the first one)
|
|
if (inLiteral && dataLines.length > 0) break;
|
|
inLiteral = true;
|
|
continue;
|
|
}
|
|
// If we hit an empty line while reading a block, that block is done.
|
|
if (inLiteral && !trimmed) break;
|
|
|
|
if (inLiteral) dataLines.push(trimmed);
|
|
}
|
|
|
|
if (dataLines.length === 0) return null;
|
|
|
|
// 2. Git Base85 Alphabet for DATA
|
|
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
|
|
const charMap: Record<string, number> = {};
|
|
for (let i = 0; i < alphabet.length; i++) charMap[alphabet[i]] = i;
|
|
|
|
const buffers: Buffer[] = [];
|
|
|
|
for (const line of dataLines) {
|
|
if (line.length === 0) continue;
|
|
|
|
// 3. Decode Line Length (Strict Git format: A-Z=1-26, a-z=27-52)
|
|
const firstChar = line[0];
|
|
let bytesInLine = 0;
|
|
const charCode = firstChar.charCodeAt(0);
|
|
|
|
if (charCode >= 65 && charCode <= 90) { // A-Z
|
|
bytesInLine = charCode - 65 + 1;
|
|
} else if (charCode >= 97 && charCode <= 122) { // a-z
|
|
bytesInLine = charCode - 97 + 27;
|
|
} else {
|
|
// Invalid or empty line length prefix, skip
|
|
continue;
|
|
}
|
|
|
|
const encodedData = line.substring(1);
|
|
const lineBuffer = Buffer.alloc(bytesInLine);
|
|
let outIdx = 0;
|
|
|
|
// Process in groups of 5 chars to produce 4 bytes
|
|
for (let i = 0; i < encodedData.length && outIdx < bytesInLine; i += 5) {
|
|
let accumulator = 0;
|
|
for (let j = 0; j < 5; j++) {
|
|
// If we run out of chars, standard git behavior is implicit padding,
|
|
// though usually the length check handles this.
|
|
const char = encodedData[i + j] || '}';
|
|
accumulator = accumulator * 85 + (charMap[char] ?? 0);
|
|
}
|
|
|
|
// Git stores these as Big Endian 32-bit ints
|
|
for (let shift = 24; shift >= 0 && outIdx < bytesInLine; shift -= 8) {
|
|
lineBuffer[outIdx++] = (accumulator >> shift) & 0xff;
|
|
}
|
|
}
|
|
buffers.push(lineBuffer);
|
|
}
|
|
|
|
const compressedBuffer = Buffer.concat(buffers);
|
|
|
|
try {
|
|
// 4. Inflate the concatenated data
|
|
return zlib.inflateSync(compressedBuffer);
|
|
} catch (e) {
|
|
console.error("Failed to inflate git binary patch:", e);
|
|
return null;
|
|
}
|
|
} |