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:
Garry Tan
2026-04-10 19:34:01 -10:00
committed by GitHub
parent 5bd4398da4
commit 8de04d3827
427 changed files with 136 additions and 131884 deletions

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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('/');