Centralize all path management via src/config/paths.ts

- Add getPaths() with workspace, scripts, ppt skin/template paths
- Export getWorkspacePath(args?) helper — removes duplicate local functions
  and stale process.cwd()/workspace fallbacks from sqlite, pdf, excel,
  image, pubmed, email, audio-transcribe (7 files)
- weather.ts: replace __dirname-relative scripts path with getPaths().scripts
- Move ppt/ to .smallclaw/skills/presenter/ppt/; update server-v2.ts,
  pptx.ts, pptx_gen.py (argparse --skin-dir/--template-dir)
- Fix PPT skin thumbnail serving: res.sendFile → fs.createReadStream
  (bypasses send module dotfiles restriction for .smallclaw/ paths)
- Fix workspace default: .smallclaw/workspace (was project root workspace/)
- Fix subagent paths to use SMALLCLAW_DATA_DIR env var
- Fix Monaco marked module load failure: restore define.amd on
  DOMContentLoaded after deferred UMD libs (tone, magenta) have loaded
- Add AudioContext resume on first user click (Chrome autoplay policy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-05-28 16:55:14 +09:00
co-authored by Claude Sonnet 4.6
parent 5608b83850
commit 2d3e33b6b3
15 changed files with 96 additions and 85 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import path from 'path';
import fs from 'fs';
import { getConfig } from '../config/config.js';
import { getWorkspacePath } from '../config/paths.js';
import { ToolResult } from '../types.js';
import { transcribeAudio } from './stt.js';
@@ -26,7 +27,7 @@ export const audioTranscribeTool = {
const filePath = String(args?.path || '').trim();
if (!filePath) return { success: false, error: 'path is required' };
const workspacePath = args?._workspacePath || args?._workspace || getConfig().getConfig()?.workspace?.path || process.cwd();
const workspacePath = getWorkspacePath(args);
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(workspacePath, filePath);
if (!fs.existsSync(resolved)) {
+2 -1
View File
@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { ToolResult } from '../types.js';
import { getConfig } from '../config/config.js';
import { getWorkspacePath } from '../config/paths.js';
// ── Config ────────────────────────────────────────────────────────────────────
@@ -333,7 +334,7 @@ export const emailReadTool = {
resolvedWorkspacePath = path.resolve(String(injectedWorkspace));
} else {
const cfg = getConfig().getConfig();
resolvedWorkspacePath = path.resolve(cfg?.workspace?.path || path.join(process.cwd(), 'workspace'));
resolvedWorkspacePath = path.resolve(getWorkspacePath(args));
}
const relDir = path.join('attachments', `uid-${uid}`);
const saveDir = path.join(resolvedWorkspacePath, relDir);
+1 -11
View File
@@ -2,21 +2,11 @@ import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs';
import { getConfig } from '../config/config.js';
import { ToolResult } from '../types.js';
import { getWorkspacePath } from '../config/paths.js';
const execFileAsync = promisify(execFile);
function getWorkspacePath(args?: any): string {
const sessionPath = args?._workspacePath || args?._workspace;
if (sessionPath) return sessionPath;
try {
return getConfig().getConfig()?.workspace?.path || path.join(process.cwd(), 'workspace');
} catch {
return path.join(process.cwd(), 'workspace');
}
}
// ─── Python scripts ────────────────────────────────────────────────────────────
const READ_SCRIPT = `
+1 -11
View File
@@ -1,18 +1,8 @@
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
import { getConfig } from '../config/config.js';
import { ToolResult } from '../types.js';
function getWorkspacePath(args?: any): string {
const sessionPath = args?._workspacePath || args?._workspace;
if (sessionPath) return sessionPath;
try {
return getConfig().getConfig()?.workspace?.path || path.join(process.cwd(), 'workspace');
} catch {
return path.join(process.cwd(), 'workspace');
}
}
import { getWorkspacePath } from '../config/paths.js';
function isPathInsideDir(base: string, target: string): boolean {
const resolvedBase = path.resolve(base);
+1 -11
View File
@@ -2,21 +2,11 @@ import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs';
import { getConfig } from '../config/config.js';
import { ToolResult } from '../types.js';
import { getWorkspacePath } from '../config/paths.js';
const execFileAsync = promisify(execFile);
function getWorkspacePath(args?: any): string {
const sessionPath = args?._workspacePath || args?._workspace;
if (sessionPath) return sessionPath;
try {
return getConfig().getConfig()?.workspace?.path || path.join(process.cwd(), 'workspace');
} catch {
return path.join(process.cwd(), 'workspace');
}
}
function isPathInsideDir(base: string, target: string): boolean {
const resolvedBase = path.resolve(base);
const resolvedTarget = path.resolve(target);
+6 -15
View File
@@ -2,26 +2,17 @@ import path from 'path';
import fs from 'fs';
import { execFile } from 'child_process';
import { getConfig } from '../config/config.js';
import { getPaths } from '../config/paths.js';
import { ToolResult } from '../types.js';
type SSESender = (type: string, data: any) => void;
// ─── Engine paths ──────────────────────────────────────────────────────────────
const PYTHON_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'pptx_gen.py');
// ─── Paths ──────────────────────────────────────────────────────────────────────
const SKIN_DIR = path.join(__dirname, '..', '..', '.smallclaw', 'skills', 'presenter', 'ppt', 'skin');
const TEMPLATE_DIR = path.join(__dirname, '..', '..', '.smallclaw', 'skills', 'presenter', 'ppt', 'template');
const LEGACY_SKIN_DIR = path.join(__dirname, '..', '..', 'assets', 'pptx_template');
// Resolve skin directory: prefer ppt/skin, fall back to legacy assets/pptx_template
const ACTIVE_SKIN_DIR = fs.existsSync(SKIN_DIR) ? SKIN_DIR
: fs.existsSync(LEGACY_SKIN_DIR) ? LEGACY_SKIN_DIR
: SKIN_DIR;
// ─── Paths (from central paths.ts) ──────────────────────────────────────────────
const { pptxScript: PYTHON_SCRIPT, pptSkinDir: SKIN_DIR, pptTemplateDir: TEMPLATE_DIR } = getPaths();
const SKIN_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
const SKIN_NAMES = fs.existsSync(ACTIVE_SKIN_DIR)
? fs.readdirSync(ACTIVE_SKIN_DIR)
const SKIN_NAMES = fs.existsSync(SKIN_DIR)
? fs.readdirSync(SKIN_DIR)
.filter(f => SKIN_EXTENSIONS.includes(path.extname(f).toLowerCase()))
.map(f => path.basename(f, path.extname(f)))
: [];
@@ -180,7 +171,7 @@ async function generateWithPython(spec: PresentationSpec, workspacePath: string)
const result = await new Promise<{ success: boolean; path?: string; folder?: string; filename?: string; slides?: number; warnings?: string[]; stdout?: string; error?: string; download_url?: string; preview_url?: string }>((resolve, reject) => {
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
execFile(pythonCmd, [PYTHON_SCRIPT, tmpSpecPath, workspacePath], {
execFile(pythonCmd, [PYTHON_SCRIPT, tmpSpecPath, workspacePath, '--skin-dir', SKIN_DIR, '--template-dir', TEMPLATE_DIR], {
timeout: 60_000,
maxBuffer: 1024 * 1024 * 10,
windowsHide: true,
+3 -2
View File
@@ -4,6 +4,7 @@ import { execFile } from 'child_process';
import { promisify } from 'util';
import { ToolResult } from '../types.js';
import { getConfig } from '../config/config.js';
import { getWorkspacePath } from '../config/paths.js';
const execFileAsync = promisify(execFile);
@@ -349,7 +350,7 @@ export const pubmedFulltextTool = {
// Download PDF
const config = getConfig().getConfig();
const workspaceDir = args?._workspacePath || args?._workspace || config.workspace?.path || path.join(process.cwd(), 'workspace');
const workspaceDir = getWorkspacePath(args);
const pdfDir = path.join(workspaceDir, 'pubmed');
fs.mkdirSync(pdfDir, { recursive: true });
@@ -466,7 +467,7 @@ export const pubmedFulltextTool = {
// Save to workspace (prefer per-user path injected by v2 executeTool)
const config = getConfig().getConfig();
const workspaceDir = args?._workspacePath || args?._workspace || config.workspace?.path || path.join(process.cwd(), 'workspace');
const workspaceDir = getWorkspacePath(args);
const savePath = args?.save_path
? path.join(workspaceDir, args.save_path)
: path.join(workspaceDir, 'pubmed', `${pmcid}.txt`);
+1 -9
View File
@@ -1,16 +1,8 @@
import path from 'path';
import fs from 'fs';
import { getConfig } from '../config/config.js';
import { getWorkspacePath } from '../config/paths.js';
import { ToolResult } from '../types.js';
function getWorkspacePath(args?: any): string {
try {
return args?._workspacePath || args?._workspace || getConfig().getConfig()?.workspace?.path || path.join(process.cwd(), 'workspace');
} catch {
return path.join(process.cwd(), 'workspace');
}
}
function isPathInsideDir(base: string, target: string): boolean {
const resolvedBase = path.resolve(base);
const resolvedTarget = path.resolve(target);
+2 -1
View File
@@ -1,6 +1,7 @@
import { ToolResult } from '../types.js';
import { getConfig } from '../config/config.js';
import { getVault } from '../security/vault.js';
import { getPaths } from '../config/paths.js';
import { spawn } from 'child_process';
import path from 'path';
import https from 'https';
@@ -1010,7 +1011,7 @@ function getCdsApiKey(): string | undefined {
function runPythonCds(input: object, timeoutMs = 300_000): Promise<any> {
return new Promise((resolve, reject) => {
const scriptPath = path.resolve(__dirname, '../../scripts/era5_cds_fetch.py');
const scriptPath = path.join(getPaths().scripts, 'era5_cds_fetch.py');
const child = spawn('python3', [scriptPath], { stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';