fix: community fix wave — 9 PRs, 8 contributors (v0.6.1) (#38)
* fix: validateSlug accepts ellipsis filenames, rejects only real path traversal Changed regex from /\.\./ to /(^|\/)\.\.($|\/)/ so filenames with "..." (like YouTube transcripts, TED talks, podcast titles) are no longer falsely rejected. The old regex matched ".." anywhere as a substring. The new one only matches ".." as a complete path component (e.g., ../foo, foo/../bar, bare ..). Fixes 1.2% silent data loss on real-world import corpora. Co-Authored-By: orendi84 <orendi84@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: import walker skips node_modules, handles broken symlinks, supports .mdx Three improvements to the file walker: - Skip node_modules directories (prevents crashes importing JS/TS projects) - try/catch around statSync for broken symlinks (warns and continues) - Accept .mdx files alongside .md (extends to slugifyPath and isSyncable) Co-Authored-By: mattbratos <mattbratos@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: init exits cleanly, auto-creates pgvector, updates Supabase UI hint Three init improvements: - process.stdin.pause() after reading URL input (prevents event loop hang) - Auto-run CREATE EXTENSION IF NOT EXISTS vector with fallback message - Update Supabase session pooler navigation hint to match current dashboard UI Co-Authored-By: changergosum <changergosum@users.noreply.github.com> Co-Authored-By: eric-hth <eric-hth@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf: parallelize keyword search with embedding pipeline Run keyword search concurrently with the embed+vector pipeline instead of sequentially. Keyword search has no embedding dependency so it can overlap with the OpenAI API call, saving ~200-500ms per search. Co-Authored-By: irresi <irresi@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update Hermes Agent link to NousResearch GitHub repo Co-Authored-By: howardpen9 <howardpen9@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add community PR wave process to CLAUDE.md Documents the fix wave workflow: categorize, deduplicate, collector branch, test, close with context, ship as one PR with attribution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version and changelog (v0.6.1) Community fix wave: 9 PRs re-implemented with full test coverage. 6 bug fixes, 1 perf improvement, 2 feature additions, 8 contributors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: migrate gstack from vendored to team mode Remove vendored .claude/skills/gstack/ from git tracking. The global install at ~/.claude/skills/gstack/ is the source of truth. Each developer runs `cd ~/.claude/skills/gstack && ./setup` to set up symlink stubs locally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: untrack skill symlink stubs These are generated locally by gstack's ./setup script. Not project code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: credit community contributors in CHANGELOG Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update OpenClaw links from .com to .ai openclaw.com is a parked page. openclaw.ai is the real product. Co-Authored-By: joshua-morris <joshua-morris@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: orendi84 <orendi84@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: mattbratos <mattbratos@users.noreply.github.com> Co-authored-by: changergosum <changergosum@users.noreply.github.com> Co-authored-by: eric-hth <eric-hth@users.noreply.github.com> Co-authored-by: irresi <irresi@users.noreply.github.com> Co-authored-by: howardpen9 <howardpen9@users.noreply.github.com> Co-authored-by: joshua-morris <joshua-morris@users.noreply.github.com>
This commit is contained in:
@@ -210,13 +210,22 @@ function collectMarkdownFiles(dir: string): string[] {
|
||||
for (const entry of readdirSync(d)) {
|
||||
// Skip hidden dirs and .raw dirs
|
||||
if (entry.startsWith('.')) continue;
|
||||
// Skip node_modules
|
||||
if (entry === 'node_modules') continue;
|
||||
|
||||
const full = join(d, entry);
|
||||
const stat = statSync(full);
|
||||
let stat;
|
||||
try {
|
||||
stat = statSync(full);
|
||||
} catch {
|
||||
// Broken symlink or permission error — skip
|
||||
console.warn(`[gbrain import] Skipping unreadable path: ${full}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.endsWith('.md')) {
|
||||
} else if (entry.endsWith('.md') || entry.endsWith('.mdx')) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,15 +57,21 @@ export async function runInit(args: string[]) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Check pgvector extension
|
||||
// Check and auto-create pgvector extension
|
||||
try {
|
||||
const conn = (engine as any).sql || (await import('../core/db.ts')).getConnection();
|
||||
const ext = await conn`SELECT extname FROM pg_extension WHERE extname = 'vector'`;
|
||||
if (ext.length === 0) {
|
||||
console.error('pgvector extension not found. Run in Supabase SQL Editor:');
|
||||
console.error(' CREATE EXTENSION vector;');
|
||||
await engine.disconnect();
|
||||
process.exit(1);
|
||||
console.log('pgvector extension not found. Attempting to create...');
|
||||
try {
|
||||
await conn`CREATE EXTENSION IF NOT EXISTS vector`;
|
||||
console.log('pgvector extension created successfully.');
|
||||
} catch {
|
||||
console.error('Could not auto-create pgvector extension. Run manually in SQL Editor:');
|
||||
console.error(' CREATE EXTENSION vector;');
|
||||
await engine.disconnect();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: proceed without pgvector check if query fails
|
||||
@@ -111,8 +117,7 @@ async function supabaseWizard(): Promise<string> {
|
||||
// Fallback to manual URL
|
||||
console.log('\nEnter your Supabase/Postgres connection URL:');
|
||||
console.log(' Format: postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres');
|
||||
console.log(' Find it: Supabase Dashboard > gear icon (Project Settings) > Database >');
|
||||
console.log(' Connection string > URI tab > change dropdown to "Session pooler"\n');
|
||||
console.log(' Find it: Supabase Dashboard > Connect (top bar) > Connection String > Session Pooler\n');
|
||||
|
||||
const url = await readLine('Connection URL: ');
|
||||
if (!url) {
|
||||
@@ -129,6 +134,7 @@ function readLine(prompt: string): Promise<string> {
|
||||
process.stdin.setEncoding('utf-8');
|
||||
process.stdin.once('data', (chunk) => {
|
||||
data = chunk.toString().trim();
|
||||
process.stdin.pause();
|
||||
resolve(data);
|
||||
});
|
||||
process.stdin.resume();
|
||||
|
||||
@@ -627,7 +627,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
// Helpers
|
||||
function validateSlug(slug: string): string {
|
||||
// Git is the system of record — slugs are lowercased repo-relative paths.
|
||||
if (!slug || /\.\./.test(slug) || /^\//.test(slug)) {
|
||||
if (!slug || /(^|\/)\.\.($|\/)/.test(slug) || /^\//.test(slug)) {
|
||||
throw new Error(`Invalid slug: "${slug}". Slugs cannot be empty, start with /, or contain path traversal.`);
|
||||
}
|
||||
// Normalize to lowercase — all entry points (pathToSlug, inferSlug, frontmatter, direct writes) go through here
|
||||
|
||||
@@ -36,17 +36,17 @@ export async function hybridSearch(
|
||||
}
|
||||
}
|
||||
|
||||
// Embed all query variants
|
||||
const embeddings = await Promise.all(queries.map(q => embed(q)));
|
||||
// Run keyword search concurrently with embed+vector pipeline
|
||||
const [keywordResults, embeddings] = await Promise.all([
|
||||
engine.searchKeyword(query, { limit: limit * 2 }),
|
||||
Promise.all(queries.map(q => embed(q))),
|
||||
]);
|
||||
|
||||
// Run vector search for each embedding
|
||||
const vectorLists = await Promise.all(
|
||||
embeddings.map(emb => engine.searchVector(emb, { limit: limit * 2 })),
|
||||
);
|
||||
|
||||
// Run keyword search (only the original query)
|
||||
const keywordResults = await engine.searchKeyword(query, { limit: limit * 2 });
|
||||
|
||||
// Merge all result lists via RRF
|
||||
const allLists = [...vectorLists, keywordResults];
|
||||
const fused = rrfFusion(allLists);
|
||||
|
||||
@@ -76,8 +76,8 @@ export function buildSyncManifest(gitDiffOutput: string): SyncManifest {
|
||||
* Filter a file path to determine if it should be synced to GBrain.
|
||||
*/
|
||||
export function isSyncable(path: string): boolean {
|
||||
// Must be .md
|
||||
if (!path.endsWith('.md')) return false;
|
||||
// Must be .md or .mdx
|
||||
if (!path.endsWith('.md') && !path.endsWith('.mdx')) return false;
|
||||
|
||||
// Skip hidden directories
|
||||
if (path.split('/').some(p => p.startsWith('.'))) return false;
|
||||
@@ -119,7 +119,7 @@ export function slugifySegment(segment: string): string {
|
||||
* notes/v1.0.0.md → notes/v1.0.0
|
||||
*/
|
||||
export function slugifyPath(filePath: string): string {
|
||||
let path = filePath.replace(/\.md$/i, '');
|
||||
let path = filePath.replace(/\.mdx?$/i, '');
|
||||
path = path.replace(/\\/g, '/');
|
||||
path = path.replace(/^\.?\//, '');
|
||||
return path.split('/').map(slugifySegment).filter(Boolean).join('/');
|
||||
|
||||
Reference in New Issue
Block a user