migration to postgres

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 17:42:59 +00:00
co-authored by Claude Opus 4.6
parent 7f04ecd644
commit 500a70910e
54 changed files with 4016 additions and 264 deletions
+46 -8
View File
@@ -5,8 +5,9 @@ import type { Subprocess } from "bun";
import type { PiEvent, MessageCost } from "./types";
import { readApiKeys } from "../server-settings/pi-mono";
import { readSearxngConfig } from "../server-settings/searxng";
import { PI_CONFIG_DIR, DATA_PATH, getHomeDir, getGlobalSkillsDir, getUserSkillsDir, getGlobalExtensionsDir, getUserExtensionsDir, getGlobalToolsDir, getUserToolsDir, getNativeResourcesDir, getGlobalResourcesDir } from "../../data-path";
import { PI_CONFIG_DIR, DATA_PATH, SERVER_CONFIG_DIR, getHomeDir, getGlobalSkillsDir, getUserSkillsDir, getGlobalExtensionsDir, getUserExtensionsDir, getGlobalToolsDir, getUserToolsDir, getNativeResourcesDir, getGlobalResourcesDir } from "../../data-path";
import { ensureDockerContainer } from "../terminal/websocket";
import { getServerIntegration, getUserIntegration } from "officerdb";
import { logger } from "./logger";
import { parseFrontmatter } from "../skills/skills";
@@ -159,12 +160,33 @@ function buildResourcesEnv(): string {
return JSON.stringify(result);
}
function getGoogleConfigPath(): string {
return join(homedir(), '.config', 'officer.dev', 'google-oauth.json');
async function ensureGoogleConfigFile(): Promise<string> {
const filePath = join(SERVER_CONFIG_DIR, 'google-oauth.json');
try {
const integration = await getServerIntegration('google');
if (integration?.config) {
mkdirSync(SERVER_CONFIG_DIR, { recursive: true });
writeFileSync(filePath, JSON.stringify(integration.config, null, 2));
}
} catch {
// No google config available
}
return filePath;
}
function getGoogleTokenPath(email: string): string {
return join(DATA_PATH, email, 'integrations', 'google.json');
async function ensureGoogleTokenFile(userId: number, email: string): Promise<string> {
const dir = join(DATA_PATH, email, 'integrations');
const filePath = join(dir, 'google.json');
try {
const integration = await getUserIntegration(userId, 'google');
if (integration?.config) {
mkdirSync(dir, { recursive: true });
writeFileSync(filePath, JSON.stringify(integration.config, null, 2));
}
} catch {
// No user google integration available
}
return filePath;
}
type SandboxOptions = {
@@ -177,6 +199,7 @@ type SandboxOptions = {
export async function spawnPi(
cwd: string,
model: string,
userId: number,
email: string,
onEvent: PiEventHandler,
sandbox?: SandboxOptions,
@@ -217,8 +240,8 @@ export async function spawnPi(
const resourcesEnv = buildResourcesEnv();
const googleConfigHost = getGoogleConfigPath();
const googleTokenHost = join(DATA_PATH, sandbox.email, 'integrations');
const googleConfigHost = await ensureGoogleConfigFile();
await ensureGoogleTokenFile(sandbox.userId, sandbox.email);
const envFlags = [
'-e', `PI_CODING_AGENT_DIR=${containerPiConfig}`,
@@ -275,13 +298,28 @@ export async function spawnPi(
}
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
const googleConfigPath = await ensureGoogleConfigFile();
const googleTokenPath = await ensureGoogleTokenFile(userId, email);
proc = Bun.spawn(args, {
cwd,
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, ...storedKeys, HOME: getHomeDir(email), OFFICER_USER_HOME: getHomeDir(email), OFFICER_USER_ROOT: join(DATA_PATH, email), PI_CODING_AGENT_DIR: PI_CONFIG_DIR, PI_TOOLS_DIRS: toolsDirs, PI_SEARXNG_URL: searxng.url, OFFICER_RESOURCES: buildResourcesEnv(), OFFICER_GOOGLE_CONFIG_PATH: getGoogleConfigPath(), OFFICER_GOOGLE_TOKEN_PATH: getGoogleTokenPath(email), OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db') },
env: {
...process.env,
...storedKeys,
HOME: getHomeDir(email),
OFFICER_USER_HOME: getHomeDir(email),
OFFICER_USER_ROOT: join(DATA_PATH, email),
PI_CODING_AGENT_DIR: PI_CONFIG_DIR,
PI_TOOLS_DIRS: toolsDirs,
PI_SEARXNG_URL: searxng.url,
OFFICER_RESOURCES: buildResourcesEnv(),
OFFICER_GOOGLE_CONFIG_PATH: googleConfigPath,
OFFICER_GOOGLE_TOKEN_PATH: googleTokenPath,
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
},
});
logger.info('Spawned Pi locally', {
+10 -12
View File
@@ -6,22 +6,20 @@ import * as storage from './storage';
import * as piBridge from './pi-bridge';
import { join, resolve } from 'path';
import { homedir } from 'os';
import { getHomeDir, getUserSettingsFile } from '../../../servers/data-path';
import { getHomeDir } from '../../../servers/data-path';
import { getUserSettings } from 'officerdb';
import { logger } from './logger';
// Default model when no user preference is set
const DEFAULT_MODEL = 'opencode/big-pickle';
async function getUserDefaultModel(email: string): Promise<string | null> {
async function getUserDefaultModel(userId: number): Promise<string | null> {
try {
const settingsPath = getUserSettingsFile(email);
const file = Bun.file(settingsPath);
if (await file.exists()) {
const settings = await file.json();
return settings?.chat?.defaultModel || null;
}
const settings = await getUserSettings(userId);
const chat = settings?.chat as Record<string, unknown> | undefined;
return (chat?.defaultModel as string) || null;
} catch (err) {
logger.error('Failed to read user settings for default model', { email, error: String(err) });
logger.error('Failed to read user settings for default model', { userId, error: String(err) });
}
return null;
}
@@ -254,7 +252,7 @@ async function handleChat(
let modelSource = 'client-provided';
let userDefault = null;
if (!model) {
userDefault = await getUserDefaultModel(email);
userDefault = await getUserDefaultModel(userId);
if (userDefault) {
model = userDefault;
modelSource = 'user-settings';
@@ -288,7 +286,7 @@ async function handleChat(
if (!session.piProcess) {
try {
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
session.piProcess = await piBridge.spawnPi(cwd, model, email, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
session.piProcess = await piBridge.spawnPi(cwd, model, userId, email, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
logger.info('Spawned Pi process for session', { sessionId, model, cwd, sandboxed });
} catch (err) {
logger.error('Failed to spawn Pi process', { sessionId, model, error: String(err) });
@@ -363,7 +361,7 @@ async function handleResume(
const homeDir = getHomeDir(email);
const sandbox = session.sandboxed && session.userId ? { userId: session.userId, username: ws.data.username, email, homeDir } : undefined;
const onEvent = createEventHandler(sessionId, session.model, session.cwd, homeDir);
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, email, onEvent, sandbox);
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, session.userId!, email, onEvent, sandbox);
logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model, sandboxed: session.sandboxed });
} catch (err) {
logger.error('Failed to spawn Pi process for resume', { sessionId, error: String(err) });