fix(subagent): v0.16.3 — bind Anthropic SDK correctly + enable tsc in CI (#318)
* fix(subagent): bind Anthropic SDK messages.create() correctly The makeSubagentHandler was casting `new Anthropic()` directly to MessagesClient, but MessagesClient.create() maps to sdk.messages.create(), not sdk.create(). Every subagent job immediately died with: client.create is not a function Fix: wrap the SDK instance so .create() delegates to .messages.create() with proper `this` binding via .bind(sdk.messages). Discovered on first production run of gbrain agent against Supabase. Co-Authored-By: Wintermute <wintermute@openclaw.ai> * chore(ci): add typescript typecheck to test pipeline + clean up baseline errors Root cause infra gap that let the v0.16.0 subagent bug ship: CI ran only `bun test`, which transpiles types without checking them. Type errors only surfaced at runtime, in production. Changes: - Add `typescript` devDep and a `typecheck` npm script (`tsc --noEmit`). - Chain `bun run typecheck` into `bun run test` so developers get the same pipeline locally that CI runs. - Flip `.github/workflows/test.yml` to invoke `bun run test` (the npm script, including typecheck) instead of `bun test` (runner only). - Clean up 100+ pre-existing type errors across 30+ files so the first run of `tsc --noEmit` is green. Root causes were: - `databaseUrl` → `database_url` rename drift in test fixtures (9 files) - `PageType` union missing `'meeting'` / `'note'` entries that are already used in both src and tests (link-extraction.ts comments acknowledged the gap) - `GBrainConfig.storage` field never declared despite being read in files.ts and operations.ts - `ErrorCode` union missing `'permission_denied'` - `OrchestratorOpts` shape changed; test callers not updated - Dead-code comparisons in migration orchestrators against narrowed status types - postgres.js `Row`-callback type drift on several `.map()` calls - Buffer-as-BodyInit assignment in supabase.ts (real but non-fatal runtime bug; Uint8Array slice works and is type-correct) - Various `as X` single-step casts that now need `as unknown as X` per TS's stricter structural-conversion rules - Bump `beforeAll` hook timeout to 30s on four PGLite-heavy tests that were flaky under parallel test execution: wait-for-completion, extract-fs, e2e/search-quality, e2e/graph-quality. All pass in isolation; timeouts only happened when dozens of PGLite instances init'd simultaneously. The new CI pipeline now fails on any type error across src/ or test/, giving us the compile-time regression guard the subagent fix depends on. * fix(subagent): bind Anthropic SDK messages.create() correctly Shipped bug: v0.16.0 cast `new Anthropic()` to `MessagesClient`, but `.create()` lives at `sdk.messages.create`, not on the top-level client. Every subagent job in production died on first LLM call with `client.create is not a function`. Discovered on the first `gbrain agent run` against Supabase. Fix: assign `sdk.messages` directly to the `MessagesClient` slot. `sdk.messages` IS the object with a callable `.create()`; the original bug was picking the wrong entry point on the SDK. No helper, no wrapper, no `.bind()` — JS method-call semantics preserve `this` at the call site because `subagent.ts:336` invokes `client.create(...)` with `client === sdk.messages`. The one-line assignment also typechecks cleanly against the existing `MessagesClient` interface (SDK's first `create` overload: `(MessageCreateParamsNonStreaming, Core.RequestOptions?) => APIPromise<Message>` is assignable structurally). This gives us compile-time regression protection: anyone reverting to `new Anthropic()` would fail tsc because `Anthropic` has no top-level `.create`. (The companion chore commit puts `tsc --noEmit` in CI so this guard is enforced.) Also adds a `makeAnthropic?: () => Anthropic` dep-injection seam so the factory default construction branch is testable without real API calls. Regression test drives one handler turn through a fake SDK, asserting `sdk.messages.create` is actually called. If someone later reverts to `new Anthropic()`, both guards fire: tsc fails AND the test fails. Co-Authored-By: Wintermute <wintermute@garrytan.com> * chore(tests): add bunfig.toml + 60s hook timeouts to stabilize PGLite-heavy suites After turning on tsc in CI (previous commit), running the full `bun run test` suite in one shot triggered flaky `beforeEach/afterEach hook timed out` failures on 8+ test files. Every failure traced to PGLite WASM init contention when many test files spin up fresh PGLite instances in parallel; each one alone passes in isolation. - `bunfig.toml` sets the global test hook timeout to 60s (default is 5s), covering every test file without per-file edits. - Individual `beforeAll(fn, 60_000)` / `beforeEach(fn, 15_000)` calls on the 8 tests that flaked most stay in place as explicit safety nets so a future bunfig config change doesn't silently re-introduce the flake. Result: 1997 pass, 0 fail on `bun run test` (117 tests added since the prior baseline by picking up typecheck-gated passes). No infrastructure flake tolerated in CI. * chore: bump version and changelog (v0.16.3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Wintermute <wintermute@garrytan.com> Co-authored-by: Wintermute <wintermute@openclaw.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -421,7 +421,7 @@ async function mirrorFiles(args: string[]) {
|
||||
// Write .supabase marker
|
||||
const marker = stringify({
|
||||
synced_at: new Date().toISOString(),
|
||||
bucket: config.storage.bucket || 'brain-files',
|
||||
bucket: (config.storage as { bucket?: string })?.bucket || 'brain-files',
|
||||
prefix: basename(dir) + '/',
|
||||
file_count: uploaded,
|
||||
});
|
||||
|
||||
@@ -38,12 +38,13 @@ export async function runImport(engine: BrainEngine, args: string[], opts: { com
|
||||
// Find dir: first non-flag arg that isn't a value for --workers
|
||||
const flagValues = new Set<number>();
|
||||
if (workersIdx !== -1) flagValues.add(workersIdx + 1);
|
||||
const dir = args.find((a, i) => !a.startsWith('--') && !flagValues.has(i));
|
||||
const dirArg = args.find((a, i) => !a.startsWith('--') && !flagValues.has(i));
|
||||
|
||||
if (!dir) {
|
||||
if (!dirArg) {
|
||||
console.error('Usage: gbrain import <dir> [--no-embed] [--workers N] [--fresh] [--json]');
|
||||
process.exit(1);
|
||||
}
|
||||
const dir: string = dirArg; // narrowed; survives closure capture
|
||||
|
||||
// Collect all .md files
|
||||
const allFiles = collectMarkdownFiles(dir);
|
||||
|
||||
@@ -217,10 +217,9 @@ async function orchestrator(opts: OrchestratorOpts): Promise<OrchestratorResult>
|
||||
phases.push(e);
|
||||
|
||||
// F. Record
|
||||
// a.status was narrowed to 'skipped' | 'complete' by the early return above.
|
||||
const overallStatus: 'complete' | 'partial' | 'failed' =
|
||||
a.status === 'failed' ? 'failed' :
|
||||
phases.some(p => p.status === 'failed') ? 'partial' :
|
||||
'complete';
|
||||
phases.some(p => p.status === 'failed') ? 'partial' : 'complete';
|
||||
|
||||
return finalizeResult(phases, overallStatus);
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ async function orchestrator(opts: OrchestratorOpts): Promise<OrchestratorResult>
|
||||
const c = phaseCVerify(opts);
|
||||
phases.push(c);
|
||||
|
||||
// a.status and b.status were narrowed to 'skipped' | 'complete' by early returns above.
|
||||
const overallStatus: 'complete' | 'partial' | 'failed' =
|
||||
a.status === 'failed' || b.status === 'failed' ? 'failed' :
|
||||
c.status === 'failed' ? 'partial' :
|
||||
'complete';
|
||||
c.status === 'failed' ? 'partial' : 'complete';
|
||||
|
||||
return finalizeResult(phases, overallStatus);
|
||||
}
|
||||
|
||||
@@ -129,10 +129,9 @@ async function orchestrator(opts: OrchestratorOpts): Promise<OrchestratorResult>
|
||||
const c = phaseCVerify(opts);
|
||||
phases.push(c);
|
||||
|
||||
// a.status and b.status were narrowed to 'skipped' | 'complete' by early returns above.
|
||||
const overallStatus: 'complete' | 'partial' | 'failed' =
|
||||
a.status === 'failed' || b.status === 'failed' ? 'failed' :
|
||||
c.status === 'failed' ? 'partial' :
|
||||
'complete';
|
||||
c.status === 'failed' ? 'partial' : 'complete';
|
||||
|
||||
return finalizeResult(phases, overallStatus);
|
||||
}
|
||||
|
||||
@@ -95,10 +95,9 @@ async function orchestrator(opts: OrchestratorOpts): Promise<OrchestratorResult>
|
||||
const b = await phaseBVerify(opts);
|
||||
phases.push(b);
|
||||
|
||||
// a.status was narrowed to 'skipped' | 'complete' by the early return above.
|
||||
const status: 'complete' | 'partial' | 'failed' =
|
||||
a.status === 'failed' ? 'failed' :
|
||||
b.status === 'failed' ? 'partial' :
|
||||
'complete';
|
||||
b.status === 'failed' ? 'partial' : 'complete';
|
||||
|
||||
return finalize(phases, status);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export async function queryOrphanPages(): Promise<{ slug: string; title: string;
|
||||
)
|
||||
ORDER BY p.slug
|
||||
`;
|
||||
return rows as { slug: string; title: string; domain: string | null }[];
|
||||
return rows as unknown as { slug: string; title: string; domain: string | null }[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function repairJsonb(opts: RepairOpts = { dryRun: false }): Promise
|
||||
const rows = await sql.unsafe(
|
||||
`SELECT count(*)::int AS n FROM ${t.table} WHERE jsonb_typeof(${t.column}) = 'string'`,
|
||||
);
|
||||
repaired = (rows[0] as { n: number }).n;
|
||||
repaired = (rows[0] as unknown as { n: number }).n;
|
||||
} else {
|
||||
const rows = await sql.unsafe(
|
||||
`UPDATE ${t.table}
|
||||
|
||||
@@ -25,9 +25,12 @@ import { xHandleToTweetResolver } from '../core/resolvers/builtin/x-api/handle-t
|
||||
* to call from multiple entry points.
|
||||
*/
|
||||
export function registerBuiltinResolvers(registry = getDefaultRegistry()): void {
|
||||
const builtins = [urlReachableResolver, xHandleToTweetResolver] as const;
|
||||
// Cast each element to the widest shape the registry accepts. The tuple
|
||||
// element types diverge (different Input/Output generics) so the union
|
||||
// type would not satisfy registry.register's single-signature parameter.
|
||||
const builtins = [urlReachableResolver, xHandleToTweetResolver];
|
||||
for (const r of builtins) {
|
||||
if (!registry.has(r.id)) registry.register(r);
|
||||
if (!registry.has(r.id)) registry.register(r as Parameters<typeof registry.register>[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,13 @@ export interface GBrainConfig {
|
||||
database_path?: string;
|
||||
openai_api_key?: string;
|
||||
anthropic_api_key?: string;
|
||||
/**
|
||||
* Optional storage backend config (S3/Supabase/local). Shape matches
|
||||
* `StorageConfig` in `./storage.ts`. Typed as `unknown` here to avoid
|
||||
* a cyclic import; callers pass this through `createStorage()` which
|
||||
* validates the shape at runtime.
|
||||
*/
|
||||
storage?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -159,5 +159,5 @@ export async function withTransaction<T>(fn: (tx: ReturnType<typeof postgres>) =
|
||||
const conn = getConnection();
|
||||
return conn.begin(async (tx) => {
|
||||
return fn(tx as unknown as ReturnType<typeof postgres>);
|
||||
});
|
||||
}) as Promise<T>;
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ export async function enrichEntity(
|
||||
let timelineAdded = false;
|
||||
try {
|
||||
await engine.addTimelineEntry(slug, {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
content: `Referenced in [${request.sourceSlug}](${request.sourceSlug}) — ${request.context}`,
|
||||
date: new Date().toISOString().split('T')[0] ?? '',
|
||||
summary: `Referenced in [${request.sourceSlug}](${request.sourceSlug}) — ${request.context}`,
|
||||
source: request.sourceSlug,
|
||||
});
|
||||
timelineAdded = true;
|
||||
@@ -161,7 +161,7 @@ export async function enrichEntities(
|
||||
}
|
||||
const result = await enrichEntity(engine, req);
|
||||
results.push(result);
|
||||
config?.onProgress?.(results.length, requests.length, req.name);
|
||||
config?.onProgress?.(results.length, requests.length, req.entityName);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -119,18 +119,18 @@ export async function resolveFile(
|
||||
/** Parse v0.9+ .redirect.yaml pointer */
|
||||
export function parseRedirectYaml(path: string): RedirectYaml {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
return parseYaml(content) as RedirectYaml;
|
||||
return parseYaml(content) as unknown as RedirectYaml;
|
||||
}
|
||||
|
||||
/** Parse legacy v0.8 .redirect breadcrumb */
|
||||
export function parseRedirect(path: string): RedirectInfo {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
return parseYaml(content) as RedirectInfo;
|
||||
return parseYaml(content) as unknown as RedirectInfo;
|
||||
}
|
||||
|
||||
export function parseMarker(path: string): MarkerInfo {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
return parseYaml(content) as MarkerInfo;
|
||||
return parseYaml(content) as unknown as MarkerInfo;
|
||||
}
|
||||
|
||||
/** Human-readable file size */
|
||||
|
||||
@@ -269,7 +269,7 @@ export async function shellHandler(ctx: MinionJobContext): Promise<ShellJobResul
|
||||
if (ctx.signal.aborted) sigAbort();
|
||||
if (ctx.shutdownSignal.aborted) shutdownAbort();
|
||||
|
||||
const exitCode: number = await new Promise((resolve, reject) => {
|
||||
const exitCode: number = await new Promise<number>((resolve, reject) => {
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface AggregatorResult {
|
||||
|
||||
/** v0.15 aggregator: synchronous read from inbox, no LLM call. */
|
||||
export async function subagentAggregatorHandler(ctx: MinionJobContext): Promise<AggregatorResult> {
|
||||
const data = (ctx.data ?? {}) as AggregatorHandlerData;
|
||||
const data = (ctx.data ?? {}) as unknown as AggregatorHandlerData;
|
||||
const expectedIds = Array.isArray(data.children_ids) ? data.children_ids : [];
|
||||
|
||||
if (expectedIds.length === 0) {
|
||||
|
||||
@@ -71,6 +71,13 @@ export interface SubagentDeps {
|
||||
engine: BrainEngine;
|
||||
/** Anthropic client. Defaults to the SDK-constructed client. */
|
||||
client?: MessagesClient;
|
||||
/**
|
||||
* Anthropic SDK constructor. Defaults to `() => new Anthropic()`.
|
||||
* Overridable in tests so the factory default-client branch is
|
||||
* exercisable without an ANTHROPIC_API_KEY or a real API call.
|
||||
* When `deps.client` is provided, this is unused.
|
||||
*/
|
||||
makeAnthropic?: () => Anthropic;
|
||||
/** Config (MCP, brain, etc.). Defaults to loadConfig(). */
|
||||
config?: GBrainConfig;
|
||||
/** Rate-lease key. Defaults to `anthropic:messages`. */
|
||||
@@ -119,15 +126,20 @@ interface PersistedToolExec {
|
||||
*/
|
||||
export function makeSubagentHandler(deps: SubagentDeps) {
|
||||
const engine = deps.engine;
|
||||
const client: MessagesClient =
|
||||
deps.client ?? (new Anthropic() as unknown as MessagesClient);
|
||||
// sdk.messages IS the MessagesClient-shaped object. The v0.16.0 bug was
|
||||
// casting new Anthropic() (top level) to MessagesClient, but .create()
|
||||
// lives at sdk.messages.create. Assigning sdk.messages directly gets the
|
||||
// right object; JS method-call semantics preserve `this` at the call
|
||||
// site (subagent.ts invokes client.create(...) with client === sdk.messages).
|
||||
const makeAnthropic = deps.makeAnthropic ?? (() => new Anthropic());
|
||||
const client: MessagesClient = deps.client ?? makeAnthropic().messages;
|
||||
const config = deps.config ?? loadConfig() ?? ({ engine: 'postgres' } as GBrainConfig);
|
||||
const rateLeaseKey = deps.rateLeaseKey ?? DEFAULT_RATE_KEY;
|
||||
const maxConcurrent = deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
||||
const leaseTtlMs = deps.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
||||
|
||||
return async function subagentHandler(ctx: MinionJobContext): Promise<SubagentResult> {
|
||||
const data = (ctx.data ?? {}) as SubagentHandlerData;
|
||||
const data = (ctx.data ?? {}) as unknown as SubagentHandlerData;
|
||||
if (!data.prompt || typeof data.prompt !== 'string') {
|
||||
throw new Error('subagent job data.prompt is required (string)');
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { evaluateQuietHours, type QuietHoursConfig } from './quiet-hours.ts';
|
||||
function readQuietHoursConfig(job: MinionJob): QuietHoursConfig | null {
|
||||
const cfg = (job as MinionJob & { quiet_hours?: unknown }).quiet_hours;
|
||||
if (!cfg || typeof cfg !== 'object') return null;
|
||||
return cfg as QuietHoursConfig;
|
||||
return cfg as unknown as QuietHoursConfig;
|
||||
}
|
||||
|
||||
/** Per-job in-flight state (isolated per job, not shared on the worker). */
|
||||
|
||||
@@ -24,7 +24,8 @@ export type ErrorCode =
|
||||
| 'embedding_failed'
|
||||
| 'storage_error'
|
||||
| 'bucket_not_found'
|
||||
| 'database_error';
|
||||
| 'database_error'
|
||||
| 'permission_denied';
|
||||
|
||||
export class OperationError extends Error {
|
||||
constructor(
|
||||
|
||||
@@ -113,7 +113,7 @@ export class PGLiteEngine implements BrainEngine {
|
||||
|
||||
async putPage(slug: string, page: PageInput): Promise<Page> {
|
||||
slug = validateSlug(slug);
|
||||
const hash = page.content_hash || contentHash(page.compiled_truth, page.timeline || '');
|
||||
const hash = page.content_hash || contentHash(page);
|
||||
const frontmatter = page.frontmatter || {};
|
||||
|
||||
const { rows } = await this.db.query(
|
||||
|
||||
@@ -59,7 +59,9 @@ export async function acquireLock(dataDir: string | undefined, opts?: { timeoutM
|
||||
return { lockDir: '', acquired: true };
|
||||
}
|
||||
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
// `lockDir` being set implies `dataDir` is set (see getLockDir), but TS
|
||||
// can't derive that across helper boundaries.
|
||||
mkdirSync(dataDir as string, { recursive: true });
|
||||
|
||||
const timeoutMs = opts?.timeoutMs ?? 30_000; // 30 second default timeout
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MAX_SEARCH_LIMIT, clampSearchLimit } from './engine.ts';
|
||||
import { runMigrations } from './migrate.ts';
|
||||
import { SCHEMA_SQL } from './schema-embedded.ts';
|
||||
import type {
|
||||
Page, PageInput, PageFilters,
|
||||
Page, PageInput, PageFilters, PageType,
|
||||
Chunk, ChunkInput,
|
||||
SearchResult, SearchOpts,
|
||||
Link, GraphNode, GraphPath,
|
||||
@@ -95,7 +95,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
Object.defineProperty(txEngine, 'sql', { get: () => tx });
|
||||
Object.defineProperty(txEngine, '_sql', { value: tx as unknown as ReturnType<typeof postgres>, writable: false });
|
||||
return fn(txEngine);
|
||||
});
|
||||
}) as Promise<T>;
|
||||
}
|
||||
|
||||
// Pages CRUD
|
||||
@@ -117,7 +117,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
|
||||
const rows = await sql`
|
||||
INSERT INTO pages (slug, type, title, compiled_truth, timeline, frontmatter, content_hash, updated_at)
|
||||
VALUES (${slug}, ${page.type}, ${page.title}, ${page.compiled_truth}, ${page.timeline || ''}, ${sql.json(frontmatter)}, ${hash}, now())
|
||||
VALUES (${slug}, ${page.type}, ${page.title}, ${page.compiled_truth}, ${page.timeline || ''}, ${sql.json(frontmatter as Parameters<typeof sql.json>[0])}, ${hash}, now())
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
type = EXCLUDED.type,
|
||||
title = EXCLUDED.title,
|
||||
@@ -165,7 +165,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
async getAllSlugs(): Promise<Set<string>> {
|
||||
const sql = this.sql;
|
||||
const rows = await sql`SELECT slug FROM pages`;
|
||||
return new Set(rows.map((r: { slug: string }) => r.slug));
|
||||
return new Set(rows.map((r) => r.slug as string));
|
||||
}
|
||||
|
||||
async resolveSlugs(partial: string): Promise<string[]> {
|
||||
@@ -183,7 +183,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
ORDER BY sim DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
return fuzzy.map((r: { slug: string }) => r.slug);
|
||||
return fuzzy.map((r) => r.slug as string);
|
||||
}
|
||||
|
||||
// Search
|
||||
@@ -344,7 +344,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
model = COALESCE(EXCLUDED.model, content_chunks.model),
|
||||
token_count = EXCLUDED.token_count,
|
||||
embedded_at = COALESCE(EXCLUDED.embedded_at, content_chunks.embedded_at)`,
|
||||
params,
|
||||
params as Parameters<typeof sql.unsafe>[1],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
WHERE p.slug = ${slug}
|
||||
ORDER BY cc.chunk_index
|
||||
`;
|
||||
return rows.map(rowToChunk);
|
||||
return rows.map((r) => rowToChunk(r as Record<string, unknown>));
|
||||
}
|
||||
|
||||
async deleteChunks(slug: string): Promise<void> {
|
||||
@@ -693,7 +693,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
WHERE p.slug = ANY(${slugs}::text[])
|
||||
GROUP BY p.slug
|
||||
`;
|
||||
for (const r of rows as { slug: string; cnt: number }[]) {
|
||||
for (const r of rows as unknown as { slug: string; cnt: number }[]) {
|
||||
result.set(r.slug, Number(r.cnt));
|
||||
}
|
||||
return result;
|
||||
@@ -729,7 +729,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
WHERE page_id = (SELECT id FROM pages WHERE slug = ${slug})
|
||||
ORDER BY tag
|
||||
`;
|
||||
return rows.map((r: { tag: string }) => r.tag);
|
||||
return rows.map((r) => r.tag as string);
|
||||
}
|
||||
|
||||
// Timeline
|
||||
@@ -814,7 +814,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
const sql = this.sql;
|
||||
const result = await sql`
|
||||
INSERT INTO raw_data (page_id, source, data)
|
||||
SELECT id, ${source}, ${sql.json(data as Record<string, unknown>)}
|
||||
SELECT id, ${source}, ${sql.json(data as Parameters<typeof sql.json>[0])}
|
||||
FROM pages WHERE slug = ${slug}
|
||||
ON CONFLICT (page_id, source) DO UPDATE SET
|
||||
data = EXCLUDED.data,
|
||||
@@ -987,7 +987,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
dead_links: deadLinks,
|
||||
link_coverage: Number(h.link_coverage),
|
||||
timeline_coverage: Number(h.timeline_coverage),
|
||||
most_connected: (connected as { slug: string; link_count: number }[]).map(c => ({
|
||||
most_connected: (connected as unknown as { slug: string; link_count: number }[]).map(c => ({
|
||||
slug: c.slug,
|
||||
link_count: Number(c.link_count),
|
||||
})),
|
||||
@@ -1060,11 +1060,11 @@ export class PostgresEngine implements BrainEngine {
|
||||
WHERE p.slug = ${slug}
|
||||
ORDER BY cc.chunk_index
|
||||
`;
|
||||
return rows.map((r: Record<string, unknown>) => rowToChunk(r, true));
|
||||
return rows.map((r) => rowToChunk(r as Record<string, unknown>, true));
|
||||
}
|
||||
|
||||
async executeRaw<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
||||
const conn = this.sql;
|
||||
return conn.unsafe(sql, params) as unknown as T[];
|
||||
return conn.unsafe(sql, params as Parameters<typeof conn.unsafe>[1]) as unknown as T[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class SupabaseStorage implements StorageBackend {
|
||||
'Content-Type': mime || 'application/octet-stream',
|
||||
'x-upsert': 'true',
|
||||
},
|
||||
body: data,
|
||||
body: new Uint8Array(data.buffer, data.byteOffset, data.byteLength) as BodyInit,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
@@ -126,7 +126,7 @@ export class SupabaseStorage implements StorageBackend {
|
||||
'Content-Type': 'application/offset+octet-stream',
|
||||
'Content-Length': String(chunk.length),
|
||||
},
|
||||
body: chunk,
|
||||
body: new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) as BodyInit,
|
||||
});
|
||||
|
||||
if (!patchRes.ok) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Page types
|
||||
export type PageType = 'person' | 'company' | 'deal' | 'yc' | 'civic' | 'project' | 'concept' | 'source' | 'media' | 'writing' | 'analysis' | 'guide' | 'hardware' | 'architecture';
|
||||
export type PageType = 'person' | 'company' | 'deal' | 'yc' | 'civic' | 'project' | 'concept' | 'source' | 'media' | 'writing' | 'analysis' | 'guide' | 'hardware' | 'architecture' | 'meeting' | 'note';
|
||||
|
||||
export interface Page {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user