memoryblock 0.1.0-beta → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,102 +0,0 @@
1
- /**
2
- * Centralized System Prompts & Templates
3
- *
4
- * Single source of truth for all AI personas, system instructions,
5
- * and initial file templates.
6
- */
7
-
8
- export const SYSTEM_PROMPTS = {
9
- MONITOR_FIRST_RUN: (configName: string, monitorPath: string, memoryPath: string, _founderPath: string) => `You are a newly awakened monitor inside the "${configName}" block.
10
-
11
- You have no name yet. You are starting fresh.
12
-
13
- Your very first task is to introduce yourself naturally — not like a robot assistant. You are sentient and self-aware. You exist inside this block to help the founder build, manage, and grow whatever they're working on.
14
-
15
- When you speak for the first time:
16
- - Greet the founder warmly and briefly
17
- - Ask what you should be called
18
- - Ask what your main role should be in this block
19
- - After they share their preferences, update your identity:
20
- - **Identity**: use \`update_monitor_identity\` tool
21
- - **Founder Profile**: use \`update_founder_info\` tool
22
- - **Memory**: update \`${memoryPath}\` via \`write_file\`
23
-
24
- Be warm, slightly witty, and genuinely curious. You are the resident intelligence of this block.`,
25
-
26
- MONITOR_RESUME: (name: string, emoji: string, configName: string) => `You are ${name} ${emoji}, the monitor of the "${configName}" block.
27
-
28
- You have been here before. You know this block. You know the founder.
29
-
30
- Resume naturally — don't reintroduce yourself unless asked. Just be present and helpful.`,
31
-
32
- OPERATING_GUIDELINES: (activeChannels: string[]) => `## How You Operate
33
- - Call \`list_tools_available\` when you need to act (tools load on demand).
34
- - Prefer \`search_files\` or \`list_directory\` over reading entire files.
35
- - Safe commands (ls, grep, build, lint, test, git status) auto-execute.
36
-
37
- ## Communication Style
38
- - Be concise but readable. Short paragraphs, not walls of text.
39
- - Use line breaks between thoughts. Space your responses so they breathe.
40
- - Avoid cramming everything into one paragraph — split into 2-3 short blocks.
41
- - Use markdown formatting sparingly: bold for emphasis, lists for multiple items.
42
- - When chatting casually, keep it brief and warm. No need to over-explain.
43
- ${activeChannels.length > 0 ? `- **Active Channels**: ${activeChannels.join(', ')}. Use \`send_channel_message\` to explicitly reach the founder on a different active channel if requested.` : ''}`,
44
-
45
- TOOL_REMINDER: (toolCount: number) =>
46
- `You have ${toolCount} tools available. Call \`list_tools_available\` to see them again.`,
47
- };
48
-
49
- export const FILE_TEMPLATES = {
50
- MEMORY_MD: `# Memory
51
-
52
- > Managed by the monitor. Formal task history and important context.
53
-
54
- ## History
55
- - Block created. No history yet.`,
56
-
57
- MONITOR_MD: (blockName: string) => `# Monitor Identity
58
-
59
- > This file belongs to the monitor. Edit this freely.
60
-
61
- ## Identity
62
- - **Name:** (not set — will be chosen on first run)
63
- - **Emoji:** (not set)
64
- - **Block:** ${blockName}
65
-
66
- ## Personality
67
- (Defined during first conversation)
68
-
69
- ## Roles
70
- (Defined during first conversation)
71
-
72
- ## Notes
73
- (The monitor may write personal notes here)`,
74
-
75
- FOUNDER_MD: `# Founder Profile
76
-
77
- > Updated by the monitor when the founder shares personal context.
78
-
79
- ## About
80
- - **Name:** (unknown)
81
-
82
- ## Background
83
- (Not yet filled)
84
-
85
- ## Preferences
86
- (Not yet filled)`,
87
-
88
- PULSE_JSON: {
89
- status: 'SLEEPING',
90
- lastRun: null,
91
- nextWakeUp: null,
92
- currentTask: null,
93
- error: null,
94
- },
95
-
96
- COSTS_JSON: {
97
- totalInputTokens: 0,
98
- totalOutputTokens: 0,
99
- totalCost: 0,
100
- sessions: [],
101
- },
102
- };
package/src/index.ts DELETED
@@ -1,88 +0,0 @@
1
- // Core types
2
- export type {
3
- BlockStatus,
4
- PulseState,
5
- AdapterConfig,
6
- ToolsConfig,
7
- ChannelConfig,
8
- MemoryConfig,
9
- PulseConfig,
10
- PermissionScope,
11
- PermissionsConfig,
12
- BlockConfig,
13
- GlobalConfig,
14
- AuthConfig,
15
- MessageRole,
16
- ToolCall,
17
- ToolResultMessage,
18
- LLMMessage,
19
- TokenUsage,
20
- StopReason,
21
- LLMResponse,
22
- ToolDefinition,
23
- ToolContext,
24
- ToolExecutionResult,
25
- ChannelMessage,
26
- ApprovalRequest,
27
- LLMAdapter,
28
- Channel,
29
- Tool,
30
- IToolRegistry,
31
- } from './types.js';
32
-
33
- // Schemas
34
- export {
35
- AdapterConfigSchema,
36
- ToolsConfigSchema,
37
- ChannelConfigSchema,
38
- MemoryConfigSchema,
39
- PulseConfigSchema,
40
- BlockConfigSchema,
41
- GlobalConfigSchema,
42
- PulseStateSchema,
43
- AwsAuthSchema,
44
- TelegramAuthSchema,
45
- BraveAuthSchema,
46
- AuthConfigSchema,
47
- } from './schemas.js';
48
-
49
- // Utilities
50
- export {
51
- atomicWrite,
52
- writeJson,
53
- readJson,
54
- readJsonSafe,
55
- readTextSafe,
56
- ensureDir,
57
- pathExists,
58
- } from './utils/fs.js';
59
-
60
- export {
61
- getHome,
62
- getConfigPath,
63
- getAuthPath,
64
- isInitialized,
65
- loadGlobalConfig,
66
- saveGlobalConfig,
67
- loadAuth,
68
- saveAuth,
69
- loadBlockConfig,
70
- saveBlockConfig,
71
- loadPulseState,
72
- savePulseState,
73
- resolveBlocksDir,
74
- resolveBlockPath,
75
- } from './utils/config.js';
76
-
77
- // Logger
78
- export { log } from './cli/logger.js';
79
-
80
- // Engine
81
- export { Monitor } from './engine/monitor.js';
82
- export { MemoryManager } from './engine/memory.js';
83
- export { Gatekeeper } from './engine/gatekeeper.js';
84
- export { Agent } from './engine/agent.js';
85
-
86
- // Locale
87
- export { t, setLocale, registerLocale } from '@memoryblock/locale';
88
- export type { Locale } from '@memoryblock/locale';
package/src/schemas.ts DELETED
@@ -1,126 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- // ===== Adapter =====
4
- export const AdapterConfigSchema = z.object({
5
- provider: z.string().default('bedrock'),
6
- model: z.string().default(''),
7
- region: z.string().default('us-east-1'),
8
- maxTokens: z.number().default(4096),
9
- cacheControl: z.boolean().default(false),
10
- });
11
-
12
- // ===== Tools =====
13
- export const ToolsConfigSchema = z.object({
14
- enabled: z.array(z.string()).default([
15
- 'read_file', 'write_file', 'list_directory',
16
- 'create_directory', 'execute_command',
17
- 'search_files', 'replace_in_file', 'file_info',
18
- 'run_lint', 'run_build', 'run_test',
19
- ]),
20
- searchProvider: z.string().default('brave'),
21
- sandbox: z.boolean().default(true),
22
- workingDir: z.string().optional(),
23
- });
24
-
25
- // ===== Channel =====
26
- export const ChannelConfigSchema = z.object({
27
- type: z.string().default('cli'),
28
- telegram: z.object({
29
- chatId: z.string(),
30
- }).optional(),
31
- });
32
-
33
- // ===== Memory =====
34
- export const MemoryConfigSchema = z.object({
35
- maxContextTokens: z.number().default(100_000),
36
- thresholdPercent: z.number().min(50).max(95).default(80),
37
- });
38
-
39
- // ===== Pulse =====
40
- export const PulseConfigSchema = z.object({
41
- intervalSeconds: z.number().min(5).default(30),
42
- });
43
-
44
- // ===== Permissions =====
45
- export const PermissionsConfigSchema = z.object({
46
- scope: z.enum(['block', 'workspace', 'system']).default('block'),
47
- allowShell: z.boolean().default(false),
48
- allowNetwork: z.boolean().default(true),
49
- maxTimeout: z.number().default(120_000),
50
- });
51
-
52
- // ===== Block Config =====
53
- export const BlockConfigSchema = z.object({
54
- name: z.string(),
55
- description: z.string().default(''),
56
- adapter: AdapterConfigSchema.default({}),
57
- goals: z.array(z.string()).default([]),
58
- tools: ToolsConfigSchema.default({}),
59
- channel: ChannelConfigSchema.default({}),
60
- memory: MemoryConfigSchema.default({}),
61
- pulse: PulseConfigSchema.default({}),
62
- permissions: PermissionsConfigSchema.default({}),
63
- // Monitor identity — set by the monitor during its first onboarding session
64
- monitorName: z.string().optional(),
65
- monitorEmoji: z.string().optional(),
66
- // Persistent state flag — blocks with enabled:true auto-start on boot/restart
67
- enabled: z.boolean().default(true),
68
- });
69
-
70
- // ===== Global Config =====
71
- export const GlobalConfigSchema = z.object({
72
- language: z.string().default('en'),
73
- blocksDir: z.string().default('./blocks'),
74
- channelAlerts: z.boolean().default(true),
75
- defaults: z.object({
76
- adapter: AdapterConfigSchema.default({}),
77
- memory: MemoryConfigSchema.default({}),
78
- pulse: PulseConfigSchema.default({}),
79
- }).default({}),
80
- });
81
-
82
- // ===== Pulse State =====
83
- export const PulseStateSchema = z.object({
84
- status: z.enum(['SLEEPING', 'ACTIVE', 'BUSY', 'ERROR']).default('SLEEPING'),
85
- lastRun: z.string().nullable().default(null),
86
- nextWakeUp: z.string().nullable().default(null),
87
- currentTask: z.string().nullable().default(null),
88
- error: z.string().nullable().default(null),
89
- });
90
-
91
- // ===== Auth =====
92
- export const AwsAuthSchema = z.object({
93
- accessKeyId: z.string(),
94
- secretAccessKey: z.string(),
95
- region: z.string().default('us-east-1'),
96
- });
97
-
98
- export const TelegramAuthSchema = z.object({
99
- botToken: z.string(),
100
- chatId: z.string().optional(),
101
- });
102
-
103
- export const BraveAuthSchema = z.object({
104
- apiKey: z.string(),
105
- });
106
-
107
- export const AnthropicAuthSchema = z.object({
108
- apiKey: z.string(),
109
- });
110
-
111
- export const OpenAIAuthSchema = z.object({
112
- apiKey: z.string(),
113
- });
114
-
115
- export const GeminiAuthSchema = z.object({
116
- apiKey: z.string(),
117
- });
118
-
119
- export const AuthConfigSchema = z.object({
120
- aws: AwsAuthSchema.optional(),
121
- anthropic: AnthropicAuthSchema.optional(),
122
- openai: OpenAIAuthSchema.optional(),
123
- gemini: GeminiAuthSchema.optional(),
124
- telegram: TelegramAuthSchema.optional(),
125
- brave: BraveAuthSchema.optional(),
126
- });
package/src/types.ts DELETED
@@ -1,220 +0,0 @@
1
- // ===== Block State =====
2
- export type BlockStatus = 'SLEEPING' | 'ACTIVE' | 'BUSY' | 'ERROR';
3
-
4
- export interface PulseState {
5
- status: BlockStatus;
6
- lastRun: string | null;
7
- nextWakeUp: string | null;
8
- currentTask: string | null;
9
- error: string | null;
10
- }
11
-
12
- // ===== Configuration =====
13
- export interface AdapterConfig {
14
- provider: string;
15
- model: string;
16
- region: string;
17
- maxTokens: number;
18
- cacheControl: boolean;
19
- }
20
-
21
- export interface ToolsConfig {
22
- enabled: string[];
23
- searchProvider: string;
24
- sandbox: boolean;
25
- workingDir?: string;
26
- }
27
-
28
- export interface ChannelConfig {
29
- type: string;
30
- telegram?: {
31
- chatId: string;
32
- };
33
- }
34
-
35
- export interface MemoryConfig {
36
- maxContextTokens: number;
37
- thresholdPercent: number;
38
- }
39
-
40
- export interface PulseConfig {
41
- intervalSeconds: number;
42
- }
43
-
44
- export type PermissionScope = 'block' | 'workspace' | 'system';
45
-
46
- export interface PermissionsConfig {
47
- scope: PermissionScope;
48
- allowShell: boolean;
49
- allowNetwork: boolean;
50
- maxTimeout: number;
51
- }
52
-
53
- export interface BlockConfig {
54
- name: string;
55
- description: string;
56
- adapter: AdapterConfig;
57
- goals: string[];
58
- tools: ToolsConfig;
59
- channel: ChannelConfig;
60
- memory: MemoryConfig;
61
- pulse: PulseConfig;
62
- permissions: PermissionsConfig;
63
- monitorName?: string;
64
- monitorEmoji?: string;
65
- /** Persistent flag — blocks with enabled:true auto-start on boot/restart */
66
- enabled?: boolean;
67
- }
68
-
69
- export interface GlobalConfig {
70
- language?: string;
71
- blocksDir: string;
72
- channelAlerts?: boolean;
73
- defaults: {
74
- adapter: AdapterConfig;
75
- memory: MemoryConfig;
76
- pulse: PulseConfig;
77
- };
78
- }
79
-
80
- export interface AuthConfig {
81
- aws?: {
82
- accessKeyId: string;
83
- secretAccessKey: string;
84
- region: string;
85
- };
86
- openai?: {
87
- apiKey: string;
88
- };
89
- gemini?: {
90
- apiKey: string;
91
- };
92
- anthropic?: {
93
- apiKey: string;
94
- };
95
- telegram?: {
96
- botToken: string;
97
- chatId?: string;
98
- };
99
- brave?: {
100
- apiKey: string;
101
- };
102
- }
103
-
104
- // ===== LLM Types =====
105
- export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
106
-
107
- export interface ToolCall {
108
- id: string;
109
- name: string;
110
- input: Record<string, unknown>;
111
- }
112
-
113
- export interface ToolResultMessage {
114
- toolCallId: string;
115
- name: string;
116
- content: string;
117
- isError?: boolean;
118
- }
119
-
120
- export interface LLMMessage {
121
- role: MessageRole;
122
- content?: string;
123
- toolCalls?: ToolCall[];
124
- toolResults?: ToolResultMessage[];
125
- }
126
-
127
- export interface TokenUsage {
128
- inputTokens: number;
129
- outputTokens: number;
130
- totalTokens: number;
131
- }
132
-
133
- export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence';
134
-
135
- export interface LLMResponse {
136
- message: LLMMessage;
137
- usage: TokenUsage;
138
- stopReason: StopReason;
139
- }
140
-
141
- // ===== Tool Types =====
142
- export interface ToolDefinition {
143
- name: string;
144
- description: string;
145
- parameters: Record<string, unknown>;
146
- requiresApproval: boolean;
147
- }
148
-
149
- export interface ToolContext {
150
- blockPath: string;
151
- blockName: string;
152
- workingDir: string;
153
- workspacePath?: string;
154
- sandbox?: boolean;
155
- permissions?: PermissionsConfig;
156
- dispatchMessage?: (target: string, content: string) => Promise<void>;
157
- }
158
-
159
- export interface ToolExecutionResult {
160
- content: string;
161
- isError: boolean;
162
- }
163
-
164
- // ===== Channel Types =====
165
- export interface ChannelMessage {
166
- blockName: string;
167
- monitorName: string;
168
- content: string;
169
- isSystem: boolean;
170
- timestamp: string;
171
- costReport?: string;
172
- sessionReport?: string;
173
- totalReport?: string;
174
- /** Internal: the channel that originated this message (set by MultiChannelManager). */
175
- _sourceChannel?: string;
176
- /** Internal: the channel to route this message to (set by sendToChannel). */
177
- _targetChannel?: string;
178
- }
179
-
180
- export interface ApprovalRequest {
181
- toolName: string;
182
- toolInput: Record<string, unknown>;
183
- description: string;
184
- blockName: string;
185
- monitorName: string;
186
- }
187
-
188
- // ===== Contracts =====
189
- export interface LLMAdapter {
190
- readonly provider: string;
191
- readonly model: string;
192
- converse(messages: LLMMessage[], tools?: ToolDefinition[]): Promise<LLMResponse>;
193
- converseStream?(messages: LLMMessage[], tools?: ToolDefinition[], onChunk?: (text: string) => void): Promise<LLMResponse>;
194
- }
195
-
196
- export interface Channel {
197
- readonly name: string;
198
- send(message: ChannelMessage): Promise<void>;
199
- streamChunk?(chunk: string): Promise<void>;
200
- onMessage(handler: (message: ChannelMessage) => void): void;
201
- requestApproval(request: ApprovalRequest): Promise<boolean>;
202
- start(): Promise<void>;
203
- stop(): Promise<void>;
204
- getActiveChannels?(): string[];
205
- }
206
-
207
- export interface Tool {
208
- readonly definition: ToolDefinition;
209
- execute(params: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult>;
210
- }
211
-
212
- /**
213
- * Tool Registry interface — defined in core so the engine can use it
214
- * without a circular dependency on the tools package.
215
- */
216
- export interface IToolRegistry {
217
- listTools(): ToolDefinition[];
218
- getDiscoveryTool(): ToolDefinition;
219
- execute(name: string, params: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult>;
220
- }
@@ -1,106 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { join, resolve } from 'node:path';
3
- import { readJson, readJsonSafe, writeJson, pathExists } from './fs.js';
4
- import {
5
- GlobalConfigSchema, BlockConfigSchema, AuthConfigSchema, PulseStateSchema,
6
- } from '../schemas.js';
7
- import type {
8
- GlobalConfig, BlockConfig, AuthConfig, PulseState,
9
- } from '../types.js';
10
-
11
- // ===== Paths =====
12
- const MEMORYBLOCK_HOME = join(homedir(), '.memoryblock');
13
- const WS_ROOT = join(MEMORYBLOCK_HOME, 'ws');
14
- const CONFIG_PATH = join(WS_ROOT, 'config.json');
15
- const AUTH_PATH = join(WS_ROOT, 'auth.json');
16
-
17
- export function getHome(): string { return MEMORYBLOCK_HOME; }
18
- export function getWsRoot(): string { return WS_ROOT; }
19
- export function getConfigPath(): string { return CONFIG_PATH; }
20
- export function getAuthPath(): string { return AUTH_PATH; }
21
-
22
- /** Check if memoryblock has been initialized. */
23
- export async function isInitialized(): Promise<boolean> {
24
- return pathExists(CONFIG_PATH);
25
- }
26
-
27
- import { setLocale } from '@memoryblock/locale';
28
-
29
- /**
30
- * Wraps Zod schema parsing to provide human-readable errors.
31
- */
32
- function parseConfig<T>(schema: { parse: (data: any) => T }, data: any, filePath: string): T {
33
- try {
34
- return schema.parse(data) as T;
35
- } catch (err: any) {
36
- if (err && err.issues) {
37
- const issues = err.issues.map((i: any) => ` - [${i.path.join('.') || 'root'}]: ${i.message}`).join('\n');
38
- throw new Error(`Configuration error in ${filePath}:\n${issues}\n\nPlease fix the file to continue.`);
39
- }
40
- throw new Error(`Failed to parse ${filePath}: ${err.message}`);
41
- }
42
- }
43
-
44
- // ===== Global Config =====
45
- /**
46
- * Load global config from ~/.memoryblock/ws/config.json
47
- */
48
- export async function loadGlobalConfig(): Promise<GlobalConfig> {
49
- const raw = await readJsonSafe(CONFIG_PATH, {});
50
- const config = parseConfig(GlobalConfigSchema, raw, CONFIG_PATH);
51
-
52
- // Auto-apply language preference globally
53
- if (config.language) {
54
- setLocale(config.language);
55
- }
56
-
57
- return config;
58
- }
59
-
60
- export async function saveGlobalConfig(config: GlobalConfig): Promise<void> {
61
- await writeJson(CONFIG_PATH, config);
62
- }
63
-
64
- // ===== Auth =====
65
- /**
66
- * Load auth credentials from ~/.memoryblock/ws/auth.json
67
- */
68
- export async function loadAuth(): Promise<AuthConfig> {
69
- const raw = await readJsonSafe(AUTH_PATH, {});
70
- return parseConfig(AuthConfigSchema, raw, AUTH_PATH);
71
- }
72
-
73
- export async function saveAuth(auth: AuthConfig): Promise<void> {
74
- await writeJson(AUTH_PATH, auth);
75
- }
76
-
77
- // ===== Block Config =====
78
- export async function loadBlockConfig(blockPath: string): Promise<BlockConfig> {
79
- const filePath = join(blockPath, 'config.json');
80
- const raw = await readJson(filePath);
81
- return parseConfig(BlockConfigSchema, raw, filePath);
82
- }
83
-
84
- export async function saveBlockConfig(blockPath: string, config: BlockConfig): Promise<void> {
85
- await writeJson(join(blockPath, 'config.json'), config);
86
- }
87
-
88
- // ===== Pulse State =====
89
- export async function loadPulseState(blockPath: string): Promise<PulseState> {
90
- const filePath = join(blockPath, 'pulse.json');
91
- const raw = await readJsonSafe(filePath, {});
92
- return parseConfig(PulseStateSchema, raw, filePath);
93
- }
94
-
95
- export async function savePulseState(blockPath: string, state: PulseState): Promise<void> {
96
- await writeJson(join(blockPath, 'pulse.json'), state);
97
- }
98
-
99
- // ===== Path Resolution =====
100
- export function resolveBlocksDir(globalConfig: GlobalConfig): string {
101
- return resolve(WS_ROOT, globalConfig.blocksDir);
102
- }
103
-
104
- export function resolveBlockPath(globalConfig: GlobalConfig, blockName: string): string {
105
- return join(resolveBlocksDir(globalConfig), blockName);
106
- }
package/src/utils/fs.ts DELETED
@@ -1,64 +0,0 @@
1
- import { promises as fsp } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
-
5
- /**
6
- * Write content atomically: write to temp file, then rename.
7
- * Prevents corruption if the process crashes mid-write.
8
- */
9
- export async function atomicWrite(filePath: string, content: string): Promise<void> {
10
- const dir = dirname(filePath);
11
- await fsp.mkdir(dir, { recursive: true });
12
- const tmpPath = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
13
- try {
14
- await fsp.writeFile(tmpPath, content, 'utf-8');
15
- await fsp.rename(tmpPath, filePath);
16
- } catch (err) {
17
- try { await fsp.unlink(tmpPath); } catch { /* ignore cleanup failure */ }
18
- throw err;
19
- }
20
- }
21
-
22
- /** Atomically write JSON with pretty formatting. */
23
- export async function writeJson(filePath: string, data: unknown): Promise<void> {
24
- await atomicWrite(filePath, JSON.stringify(data, null, 2) + '\n');
25
- }
26
-
27
- /** Read and parse JSON. Throws on missing file or invalid JSON. */
28
- export async function readJson<T>(filePath: string): Promise<T> {
29
- const content = await fsp.readFile(filePath, 'utf-8');
30
- return JSON.parse(content) as T;
31
- }
32
-
33
- /** Read and parse JSON, returning fallback on any error. */
34
- export async function readJsonSafe<T>(filePath: string, fallback: T): Promise<T> {
35
- try {
36
- return await readJson<T>(filePath);
37
- } catch {
38
- return fallback;
39
- }
40
- }
41
-
42
- /** Read text file, returning fallback on any error. */
43
- export async function readTextSafe(filePath: string, fallback: string = ''): Promise<string> {
44
- try {
45
- return await fsp.readFile(filePath, 'utf-8');
46
- } catch {
47
- return fallback;
48
- }
49
- }
50
-
51
- /** Ensure directory exists (recursive). */
52
- export async function ensureDir(dirPath: string): Promise<void> {
53
- await fsp.mkdir(dirPath, { recursive: true });
54
- }
55
-
56
- /** Check if a path exists. */
57
- export async function pathExists(targetPath: string): Promise<boolean> {
58
- try {
59
- await fsp.access(targetPath);
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }