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.
- package/README.md +221 -0
- package/dist/bin/mblk.js +0 -0
- package/package.json +61 -16
- package/src/bin/mblk.ts +0 -347
- package/src/cli/commands/create.ts +0 -62
- package/src/cli/commands/delete.ts +0 -165
- package/src/cli/commands/init.ts +0 -241
- package/src/cli/commands/permissions.ts +0 -77
- package/src/cli/commands/plugin-settings.ts +0 -111
- package/src/cli/commands/plugins.ts +0 -89
- package/src/cli/commands/reset.ts +0 -97
- package/src/cli/commands/server.ts +0 -327
- package/src/cli/commands/service.ts +0 -300
- package/src/cli/commands/start.ts +0 -843
- package/src/cli/commands/status.ts +0 -90
- package/src/cli/commands/stop.ts +0 -91
- package/src/cli/commands/web.ts +0 -74
- package/src/cli/constants.ts +0 -88
- package/src/cli/logger.ts +0 -48
- package/src/engine/agent.ts +0 -19
- package/src/engine/conversation-log.ts +0 -90
- package/src/engine/cost-tracker.ts +0 -134
- package/src/engine/gatekeeper.ts +0 -53
- package/src/engine/memory.ts +0 -87
- package/src/engine/monitor.ts +0 -719
- package/src/engine/prompts.ts +0 -102
- package/src/index.ts +0 -88
- package/src/schemas.ts +0 -126
- package/src/types.ts +0 -220
- package/src/utils/config.ts +0 -106
- package/src/utils/fs.ts +0 -64
- package/tsconfig.json +0 -10
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
5
|
-
loadGlobalConfig, resolveBlocksDir, loadPulseState, loadBlockConfig, isInitialized,
|
|
6
|
-
} from '../../utils/config.js';
|
|
7
|
-
import { pathExists } from '../../utils/fs.js';
|
|
8
|
-
import { log } from '../logger.js';
|
|
9
|
-
import { t } from '@memoryblock/locale';
|
|
10
|
-
import type { BlockStatus } from '../../types.js';
|
|
11
|
-
|
|
12
|
-
const STATUS_ICON: Record<BlockStatus, string> = {
|
|
13
|
-
SLEEPING: chalk.gray('๐ค'),
|
|
14
|
-
ACTIVE: chalk.green('๐ข'),
|
|
15
|
-
BUSY: chalk.yellow('๐ถ'),
|
|
16
|
-
ERROR: chalk.red('๐ด'),
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export async function statusCommand(): Promise<void> {
|
|
20
|
-
if (!(await isInitialized())) {
|
|
21
|
-
log.error(t.general.notInitialized);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const globalConfig = await loadGlobalConfig();
|
|
26
|
-
const blocksDir = resolveBlocksDir(globalConfig);
|
|
27
|
-
|
|
28
|
-
if (!(await pathExists(blocksDir))) {
|
|
29
|
-
log.info(t.general.noBlocksDir);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const entries = await fsp.readdir(blocksDir, { withFileTypes: true });
|
|
34
|
-
const blocks = entries.filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'));
|
|
35
|
-
|
|
36
|
-
log.brand(`Status โ ${blocks.length} block(s)\n`);
|
|
37
|
-
|
|
38
|
-
if (blocks.length === 0) {
|
|
39
|
-
console.log(` ${chalk.dim(t.status.noActive)}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const block of blocks) {
|
|
43
|
-
const blockPath = join(blocksDir, block.name);
|
|
44
|
-
try {
|
|
45
|
-
const config = await loadBlockConfig(blockPath);
|
|
46
|
-
const pulse = await loadPulseState(blockPath);
|
|
47
|
-
const icon = STATUS_ICON[pulse.status] || 'โ';
|
|
48
|
-
|
|
49
|
-
console.log(` ${icon} ${chalk.bold(config.name)} ${chalk.dim(`(${pulse.status})`)}`);
|
|
50
|
-
if (pulse.currentTask) {
|
|
51
|
-
console.log(` ${chalk.dim('Task:')} ${pulse.currentTask}`);
|
|
52
|
-
}
|
|
53
|
-
if (pulse.lastRun) {
|
|
54
|
-
console.log(` ${chalk.dim('Last:')} ${pulse.lastRun}`);
|
|
55
|
-
}
|
|
56
|
-
if (pulse.error) {
|
|
57
|
-
console.log(` ${chalk.red('Error:')} ${pulse.error}`);
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
console.log(` โ ${chalk.bold(block.name)} ${chalk.dim(`(${t.status.invalidConfig})`)}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Show archived blocks
|
|
65
|
-
const archiveDir = join(blocksDir, '_archive');
|
|
66
|
-
if (await pathExists(archiveDir)) {
|
|
67
|
-
try {
|
|
68
|
-
const archiveEntries = await fsp.readdir(archiveDir, { withFileTypes: true });
|
|
69
|
-
const archived = archiveEntries.filter(e => e.isDirectory());
|
|
70
|
-
|
|
71
|
-
if (archived.length > 0) {
|
|
72
|
-
console.log('');
|
|
73
|
-
console.log(` ${chalk.dim(t.status.archived(archived.length))}`);
|
|
74
|
-
for (const a of archived) {
|
|
75
|
-
// Extract block name and date from "name_2026-03-21T10-33-24-242Z"
|
|
76
|
-
const match = a.name.match(/^(.*?)_(\d{4}-\d{2}-\d{2})T/);
|
|
77
|
-
const name = match ? match[1] : a.name;
|
|
78
|
-
const date = match ? match[2] : '';
|
|
79
|
-
console.log(` ${chalk.dim('ยท')} ${chalk.dim(name)}${date ? chalk.dim(` (${date})`) : ''}`);
|
|
80
|
-
}
|
|
81
|
-
console.log('');
|
|
82
|
-
console.log(` ${chalk.dim(t.status.restoreHint)}`);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Archive dir exists but can't be read โ skip
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log('');
|
|
90
|
-
}
|
package/src/cli/commands/stop.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { GlobalConfig } from '../../types.js';
|
|
2
|
-
import { loadGlobalConfig, resolveBlocksDir, resolveBlockPath, savePulseState, loadPulseState, loadBlockConfig, saveBlockConfig } from '../../utils/config.js';
|
|
3
|
-
import { pathExists } from '../../utils/fs.js';
|
|
4
|
-
import { log } from '../logger.js';
|
|
5
|
-
import { promises as fsp } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
const DAEMON_PKG = '@memoryblock/daemon';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Stop a running block monitor.
|
|
12
|
-
* Updates pulse state to SLEEPING.
|
|
13
|
-
* In the MVP foreground model, this is mainly used for cleanup.
|
|
14
|
-
*/
|
|
15
|
-
export async function stopCommand(blockName?: string, options?: { preserveEnabled?: boolean }): Promise<void> {
|
|
16
|
-
const globalConfig = await loadGlobalConfig();
|
|
17
|
-
const blocksDir = resolveBlocksDir(globalConfig);
|
|
18
|
-
|
|
19
|
-
if (!await pathExists(blocksDir)) {
|
|
20
|
-
log.warn('No blocks directory found. Nothing to stop.');
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (blockName) {
|
|
25
|
-
await stopBlock(globalConfig, blockName, options);
|
|
26
|
-
} else {
|
|
27
|
-
log.brand('Stopping all blocks...\n');
|
|
28
|
-
const entries = await fsp.readdir(blocksDir, { withFileTypes: true });
|
|
29
|
-
let stopped = 0;
|
|
30
|
-
for (const entry of entries) {
|
|
31
|
-
if (entry.isDirectory()) {
|
|
32
|
-
const didStop = await stopBlock(globalConfig, entry.name, options);
|
|
33
|
-
if (didStop) stopped++;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (stopped === 0) {
|
|
37
|
-
log.dim(' No active blocks to stop.');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function stopBlock(globalConfig: GlobalConfig, name: string, options?: { preserveEnabled?: boolean }): Promise<boolean> {
|
|
43
|
-
const blockPath = resolveBlockPath(globalConfig, name);
|
|
44
|
-
|
|
45
|
-
if (!await pathExists(join(blockPath, 'pulse.json'))) {
|
|
46
|
-
log.warn(`Block "${name}" not found.`);
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pulse = await loadPulseState(blockPath);
|
|
51
|
-
|
|
52
|
-
let daemonKilled = false;
|
|
53
|
-
try {
|
|
54
|
-
const daemon = await import(DAEMON_PKG);
|
|
55
|
-
daemonKilled = await daemon.killDaemon(blockPath);
|
|
56
|
-
} catch {
|
|
57
|
-
// Daemon package not found or error parsing pid
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (pulse.status === 'SLEEPING' && !daemonKilled) {
|
|
61
|
-
log.dim(` ${name}: already sleeping`);
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
await savePulseState(blockPath, {
|
|
66
|
-
status: 'SLEEPING',
|
|
67
|
-
lastRun: new Date().toISOString(),
|
|
68
|
-
nextWakeUp: null,
|
|
69
|
-
currentTask: null,
|
|
70
|
-
error: null,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Persist disabled state across reboots (unless preserveEnabled is set)
|
|
74
|
-
if (!options?.preserveEnabled) {
|
|
75
|
-
try {
|
|
76
|
-
const blockConfig = await loadBlockConfig(blockPath);
|
|
77
|
-
if (blockConfig.enabled !== false) {
|
|
78
|
-
blockConfig.enabled = false;
|
|
79
|
-
await saveBlockConfig(blockPath, blockConfig);
|
|
80
|
-
}
|
|
81
|
-
} catch { /* config read failure is non-critical */ }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (daemonKilled) {
|
|
85
|
-
log.success(` ${name}: stopped (daemon process killed)`);
|
|
86
|
-
} else {
|
|
87
|
-
log.success(` ${name}: stopped`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return true;
|
|
91
|
-
}
|
package/src/cli/commands/web.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isInitialized,
|
|
3
|
-
} from '../../utils/config.js';
|
|
4
|
-
import { log } from '../logger.js';
|
|
5
|
-
import { DEFAULT_PORT } from '../constants.js';
|
|
6
|
-
|
|
7
|
-
const API_PKG = '@memoryblock/api';
|
|
8
|
-
|
|
9
|
-
export async function webCommand(options?: { port?: string; newToken?: boolean }): Promise<void> {
|
|
10
|
-
if (!(await isInitialized())) {
|
|
11
|
-
log.error('Not initialized. Run `mblk init` first.');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const port = parseInt(options?.port || DEFAULT_PORT, 10);
|
|
16
|
-
|
|
17
|
-
let api: any;
|
|
18
|
-
try {
|
|
19
|
-
api = await import(API_PKG);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
log.error(`Failed to load API package: ${(err as Error).message}`);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Determine workspace path for token persistence
|
|
26
|
-
const workspacePath = process.cwd();
|
|
27
|
-
|
|
28
|
-
// Generate or reuse auth token
|
|
29
|
-
const authToken = await api.generateAuthToken(workspacePath, options?.newToken);
|
|
30
|
-
|
|
31
|
-
log.brand('web\n');
|
|
32
|
-
log.dim(` http://localhost:${port}`);
|
|
33
|
-
log.dim(` token: ${authToken}`);
|
|
34
|
-
console.log('');
|
|
35
|
-
|
|
36
|
-
// Resolve web UI static files path
|
|
37
|
-
let webRoot: string | undefined;
|
|
38
|
-
try {
|
|
39
|
-
const { createRequire } = await import('node:module');
|
|
40
|
-
const require = createRequire(import.meta.url);
|
|
41
|
-
const webPkg = require.resolve('@memoryblock/web/package.json');
|
|
42
|
-
const { dirname, join } = await import('node:path');
|
|
43
|
-
webRoot = join(dirname(webPkg), 'public');
|
|
44
|
-
} catch {
|
|
45
|
-
log.warn('Web UI package not found. API-only mode.');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const server = new api.ApiServer({
|
|
49
|
-
port,
|
|
50
|
-
authToken,
|
|
51
|
-
workspacePath,
|
|
52
|
-
webRoot,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const shutdown = async () => {
|
|
56
|
-
log.system('web', 'shutting down...');
|
|
57
|
-
await server.stop();
|
|
58
|
-
process.exit(0);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
process.on('SIGINT', shutdown);
|
|
62
|
-
process.on('SIGTERM', shutdown);
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await server.start();
|
|
66
|
-
log.dim(' api server running. ctrl+c to stop.');
|
|
67
|
-
} catch (err) {
|
|
68
|
-
log.error(`API server failed: ${(err as Error).message}`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Keep alive
|
|
73
|
-
await new Promise(() => {});
|
|
74
|
-
}
|
package/src/cli/constants.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared constants for the CLI commands.
|
|
3
|
-
* Single source of truth โ imported by init.ts, start.ts, and mblk.ts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Default port for the web/API server
|
|
7
|
-
export const DEFAULT_PORT = '8420';
|
|
8
|
-
|
|
9
|
-
// Available LLM providers
|
|
10
|
-
export const PROVIDERS = [
|
|
11
|
-
{ value: 'bedrock', label: 'AWS Bedrock', hint: 'Claude, Llama via AWS' },
|
|
12
|
-
{ value: 'anthropic', label: 'Anthropic', hint: 'Claude API direct' },
|
|
13
|
-
{ value: 'openai', label: 'OpenAI', hint: 'GPT-4, GPT-4o' },
|
|
14
|
-
{ value: 'gemini', label: 'Google Gemini', hint: 'Gemini Pro, Flash' },
|
|
15
|
-
{ value: 'ollama', label: 'Ollama (local)', hint: 'Run models locally โ no API key' },
|
|
16
|
-
] as Array<{ value: string; label: string; hint: string }>;
|
|
17
|
-
|
|
18
|
-
// Available communication channels
|
|
19
|
-
export const CHANNELS = [
|
|
20
|
-
{ value: 'cli', label: 'Terminal (CLI)', hint: 'always enabled' },
|
|
21
|
-
{ value: 'web', label: 'Web Dashboard', hint: 'always enabled' },
|
|
22
|
-
{ value: 'telegram', label: 'Telegram', hint: 'bot token required' },
|
|
23
|
-
{ value: 'discord', label: 'Discord', hint: 'coming soon' },
|
|
24
|
-
{ value: 'slack', label: 'Slack', hint: 'coming soon' },
|
|
25
|
-
] as Array<{ value: string; label: string; hint: string }>;
|
|
26
|
-
|
|
27
|
-
// Optional skills & plugins
|
|
28
|
-
export const PLUGINS = [
|
|
29
|
-
{ value: 'web-search', label: 'Web Search', hint: 'search the web via Brave API' },
|
|
30
|
-
{ value: 'fetch-webpage', label: 'Fetch Webpage', hint: 'extract text from URLs' },
|
|
31
|
-
{ value: 'aws', label: 'AWS Tools', hint: 'S3, Lambda, etc.' },
|
|
32
|
-
] as Array<{ value: string; label: string; hint: string }>;
|
|
33
|
-
|
|
34
|
-
// Auth fields needed per provider
|
|
35
|
-
export const PROVIDER_AUTH: Record<string, {
|
|
36
|
-
fields: Array<{ key: string; label: string; secret: boolean }>;
|
|
37
|
-
}> = {
|
|
38
|
-
bedrock: {
|
|
39
|
-
fields: [
|
|
40
|
-
{ key: 'accessKeyId', label: 'Access Key ID', secret: true },
|
|
41
|
-
{ key: 'secretAccessKey', label: 'Secret Access Key', secret: true },
|
|
42
|
-
{ key: 'region', label: 'Region', secret: false },
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
anthropic: {
|
|
46
|
-
fields: [{ key: 'apiKey', label: 'API Key', secret: true }],
|
|
47
|
-
},
|
|
48
|
-
openai: {
|
|
49
|
-
fields: [{ key: 'apiKey', label: 'API Key', secret: true }],
|
|
50
|
-
},
|
|
51
|
-
gemini: {
|
|
52
|
-
fields: [{ key: 'apiKey', label: 'API Key', secret: true }],
|
|
53
|
-
},
|
|
54
|
-
ollama: {
|
|
55
|
-
fields: [], // No auth needed
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Auth fields for channels
|
|
60
|
-
export const CHANNEL_AUTH: Record<string, Array<{ key: string; label: string; secret: boolean }>> = {
|
|
61
|
-
telegram: [
|
|
62
|
-
{ key: 'botToken', label: 'Bot Token (from @BotFather)', secret: true },
|
|
63
|
-
{ key: 'chatId', label: 'Chat ID', secret: false },
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Read the package version from core's package.json at runtime.
|
|
69
|
-
* Falls back to 'dev' if the file can't be read.
|
|
70
|
-
*/
|
|
71
|
-
export async function getVersion(): Promise<string> {
|
|
72
|
-
try {
|
|
73
|
-
const { readFile } = await import('node:fs/promises');
|
|
74
|
-
const { join, dirname } = await import('node:path');
|
|
75
|
-
const { fileURLToPath } = await import('node:url');
|
|
76
|
-
|
|
77
|
-
// Navigate from this file to the core package.json
|
|
78
|
-
// This file is at: packages/core/src/cli/constants.ts
|
|
79
|
-
// package.json is at: packages/core/package.json
|
|
80
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
81
|
-
const pkgPath = join(thisDir, '..', '..', '..', 'package.json');
|
|
82
|
-
const raw = await readFile(pkgPath, 'utf-8');
|
|
83
|
-
const pkg = JSON.parse(raw);
|
|
84
|
-
return pkg.version || 'dev';
|
|
85
|
-
} catch {
|
|
86
|
-
return 'dev';
|
|
87
|
-
}
|
|
88
|
-
}
|
package/src/cli/logger.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
const BRAND = chalk.hex('#7C3AED');
|
|
4
|
-
const BRAND_BG = chalk.bgHex('#7C3AED').white.bold;
|
|
5
|
-
const NAME = BRAND_BG(' โฌก memoryblock ');
|
|
6
|
-
|
|
7
|
-
export const log = {
|
|
8
|
-
banner(): void {
|
|
9
|
-
/* eslint-disable no-irregular-whitespace */
|
|
10
|
-
const ascii = `
|
|
11
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
12
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
-
`;
|
|
14
|
-
/* eslint-enable no-irregular-whitespace */
|
|
15
|
-
console.log(chalk.hex('#805AD5').bold(ascii));
|
|
16
|
-
},
|
|
17
|
-
info(message: string): void {
|
|
18
|
-
console.log(`${chalk.blue('โน')} ${message}`);
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
success(message: string): void {
|
|
22
|
-
console.log(`${chalk.green('โ')} ${message}`);
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
warn(message: string): void {
|
|
26
|
-
console.log(`${chalk.yellow('โ ')} ${message}`);
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
error(message: string): void {
|
|
30
|
-
console.error(`${chalk.red('โ')} ${message}`);
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
system(blockName: string, message: string): void {
|
|
34
|
-
console.log(`${chalk.gray(`โ๏ธ [${blockName}]`)} ${message}`);
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
monitor(blockName: string, monitorName: string, message: string): void {
|
|
38
|
-
console.log(`${BRAND(`โฌก ${monitorName}`)} ${chalk.gray(`[${blockName}]`)} ${message}`);
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
brand(message: string): void {
|
|
42
|
-
console.log(`\n${NAME} ${message}`);
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
dim(message: string): void {
|
|
46
|
-
console.log(chalk.dim(message));
|
|
47
|
-
},
|
|
48
|
-
};
|
package/src/engine/agent.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent: Ephemeral worker spawned by the Monitor.
|
|
3
|
-
* Lives in blocks/<name>/agents/<agent-id>/
|
|
4
|
-
* Has its own memory and tool scope.
|
|
5
|
-
*
|
|
6
|
-
* MVP: Placeholder โ Monitor can function without agents.
|
|
7
|
-
* Full agent spawning will be implemented post-MVP.
|
|
8
|
-
*/
|
|
9
|
-
export class Agent {
|
|
10
|
-
readonly id: string;
|
|
11
|
-
readonly role: string;
|
|
12
|
-
readonly agentPath: string;
|
|
13
|
-
|
|
14
|
-
constructor(id: string, role: string, agentPath: string) {
|
|
15
|
-
this.id = id;
|
|
16
|
-
this.role = role;
|
|
17
|
-
this.agentPath = agentPath;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Conversation logger โ writes all interactions to timestamped .txt files
|
|
6
|
-
* in the block's logs/ directory.
|
|
7
|
-
*
|
|
8
|
-
* Log format:
|
|
9
|
-
* ---
|
|
10
|
-
* [2025-03-12 00:09:51] [CHANNEL:telegram] [FROM:user] [CHAT:5315436002]
|
|
11
|
-
* Hello, how are you?
|
|
12
|
-
*
|
|
13
|
-
* [2025-03-12 00:09:54] [CHANNEL:telegram] [FROM:monitor:Sam] [EMOJI:๐]
|
|
14
|
-
* I'm doing well! How can I help you today?
|
|
15
|
-
* ---
|
|
16
|
-
*/
|
|
17
|
-
export class ConversationLogger {
|
|
18
|
-
private logDir: string;
|
|
19
|
-
private logFile: string;
|
|
20
|
-
private buffer: string[] = [];
|
|
21
|
-
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
-
|
|
23
|
-
constructor(blockPath: string) {
|
|
24
|
-
this.logDir = join(blockPath, 'logs');
|
|
25
|
-
// Temporary filename until init() provides block name and channel
|
|
26
|
-
this.logFile = join(this.logDir, `session-${Date.now()}.txt`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async init(blockName: string, monitorName: string, channelType: string): Promise<void> {
|
|
30
|
-
await fsp.mkdir(this.logDir, { recursive: true });
|
|
31
|
-
|
|
32
|
-
// Build filename: {blockName}-{channelType}-{timestamp}.txt
|
|
33
|
-
const now = new Date();
|
|
34
|
-
const stamp = now.toISOString().replace('T', '_').replace(/:/g, '-').slice(0, 16);
|
|
35
|
-
this.logFile = join(this.logDir, `${blockName}-${channelType}-${stamp}.txt`);
|
|
36
|
-
|
|
37
|
-
const header = [
|
|
38
|
-
'โ'.repeat(60),
|
|
39
|
-
`SESSION START: ${new Date().toISOString()}`,
|
|
40
|
-
`Block: ${blockName}`,
|
|
41
|
-
`Monitor: ${monitorName}`,
|
|
42
|
-
`Channel: ${channelType}`,
|
|
43
|
-
'โ'.repeat(60),
|
|
44
|
-
'',
|
|
45
|
-
].join('\n');
|
|
46
|
-
await fsp.writeFile(this.logFile, header, 'utf-8');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
logUser(content: string, meta: { channel: string; chatId?: string }): void {
|
|
50
|
-
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
51
|
-
const chatPart = meta.chatId ? ` [CHAT:${meta.chatId}]` : '';
|
|
52
|
-
const entry = `[${ts}] [CHANNEL:${meta.channel}] [FROM:user]${chatPart}\n${content}\n\n`;
|
|
53
|
-
this.buffer.push(entry);
|
|
54
|
-
this.scheduleFlush();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
logMonitor(content: string, meta: { channel: string; monitorName: string; emoji?: string }): void {
|
|
58
|
-
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
59
|
-
const emojiPart = meta.emoji ? ` [EMOJI:${meta.emoji}]` : '';
|
|
60
|
-
const entry = `[${ts}] [CHANNEL:${meta.channel}] [FROM:monitor:${meta.monitorName}]${emojiPart}\n${content}\n\n`;
|
|
61
|
-
this.buffer.push(entry);
|
|
62
|
-
this.scheduleFlush();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
logSystem(message: string): void {
|
|
66
|
-
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
67
|
-
const entry = `[${ts}] [SYSTEM]\n${message}\n\n`;
|
|
68
|
-
this.buffer.push(entry);
|
|
69
|
-
this.scheduleFlush();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async close(): Promise<void> {
|
|
73
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
74
|
-
await this.flush();
|
|
75
|
-
const footer = `\n${'โ'.repeat(60)}\nSESSION END: ${new Date().toISOString()}\n${'โ'.repeat(60)}\n`;
|
|
76
|
-
await fsp.appendFile(this.logFile, footer, 'utf-8');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private scheduleFlush(): void {
|
|
80
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
81
|
-
this.flushTimer = setTimeout(() => this.flush(), 500);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private async flush(): Promise<void> {
|
|
85
|
-
if (!this.buffer.length) return;
|
|
86
|
-
const content = this.buffer.join('');
|
|
87
|
-
this.buffer = [];
|
|
88
|
-
await fsp.appendFile(this.logFile, content, 'utf-8');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import type { TokenUsage } from '../types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Pricing per 1M tokens (USD) โ system-level, zero model tokens used.
|
|
7
|
-
* Source: AWS Bedrock / Anthropic pricing pages.
|
|
8
|
-
*/
|
|
9
|
-
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
10
|
-
// Opus
|
|
11
|
-
'us.anthropic.claude-opus-4-6-v1': { input: 15, output: 75 },
|
|
12
|
-
'us.anthropic.claude-opus-4-20250514-v1:0': { input: 15, output: 75 },
|
|
13
|
-
// Sonnet
|
|
14
|
-
'us.anthropic.claude-sonnet-4-20250514-v1:0': { input: 3, output: 15 },
|
|
15
|
-
'us.anthropic.claude-sonnet-4-5-20250929-v1:0': { input: 3, output: 15 },
|
|
16
|
-
// Haiku
|
|
17
|
-
'us.anthropic.claude-3-5-haiku-20241022-v1:0': { input: 0.80, output: 4 },
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const DEFAULT_PRICING = { input: 3, output: 15 };
|
|
21
|
-
|
|
22
|
-
interface CostSnapshot {
|
|
23
|
-
sessionInput: number;
|
|
24
|
-
sessionOutput: number;
|
|
25
|
-
totalInput: number;
|
|
26
|
-
totalOutput: number;
|
|
27
|
-
sessionCost: number;
|
|
28
|
-
totalCost: number;
|
|
29
|
-
lastUpdated: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* System-level cost tracker โ tracks tokens and calculates USD cost.
|
|
34
|
-
* Persists to costs.json in the block directory.
|
|
35
|
-
* No model tokens wasted โ this is pure system bookkeeping.
|
|
36
|
-
*/
|
|
37
|
-
export class CostTracker {
|
|
38
|
-
private model: string;
|
|
39
|
-
private pricing: { input: number; output: number };
|
|
40
|
-
private sessionInput = 0;
|
|
41
|
-
private sessionOutput = 0;
|
|
42
|
-
private totalInput = 0;
|
|
43
|
-
private totalOutput = 0;
|
|
44
|
-
private turnCount = 0;
|
|
45
|
-
private lastTurnInput = 0;
|
|
46
|
-
private lastTurnOutput = 0;
|
|
47
|
-
private costFile: string;
|
|
48
|
-
|
|
49
|
-
constructor(blockPath: string, model: string) {
|
|
50
|
-
this.model = model;
|
|
51
|
-
this.costFile = join(blockPath, 'costs.json');
|
|
52
|
-
// Find pricing or use default
|
|
53
|
-
this.pricing = MODEL_PRICING[model] || DEFAULT_PRICING;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Load previous totals from costs.json. */
|
|
57
|
-
async load(): Promise<void> {
|
|
58
|
-
try {
|
|
59
|
-
const raw = await fsp.readFile(this.costFile, 'utf-8');
|
|
60
|
-
const data = JSON.parse(raw);
|
|
61
|
-
this.totalInput = data.totalInput || 0;
|
|
62
|
-
this.totalOutput = data.totalOutput || 0;
|
|
63
|
-
} catch {
|
|
64
|
-
// First run โ no file
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Track a single API call's usage. */
|
|
69
|
-
track(usage: TokenUsage): void {
|
|
70
|
-
this.lastTurnInput = usage.inputTokens;
|
|
71
|
-
this.lastTurnOutput = usage.outputTokens;
|
|
72
|
-
this.sessionInput += usage.inputTokens;
|
|
73
|
-
this.sessionOutput += usage.outputTokens;
|
|
74
|
-
this.totalInput += usage.inputTokens;
|
|
75
|
-
this.totalOutput += usage.outputTokens;
|
|
76
|
-
this.turnCount++;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Get session cost in USD. */
|
|
80
|
-
getSessionCost(): number {
|
|
81
|
-
return (this.sessionInput / 1_000_000) * this.pricing.input +
|
|
82
|
-
(this.sessionOutput / 1_000_000) * this.pricing.output;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Get total cost in USD (all sessions). */
|
|
86
|
-
getTotalCost(): number {
|
|
87
|
-
return (this.totalInput / 1_000_000) * this.pricing.input +
|
|
88
|
-
(this.totalOutput / 1_000_000) * this.pricing.output;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Format cost for display. */
|
|
92
|
-
formatCost(cost: number): string {
|
|
93
|
-
return `$${cost.toFixed(4)}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Get formatted session report. */
|
|
97
|
-
getSessionReport(): string {
|
|
98
|
-
return `${this.sessionInput.toLocaleString()} in / ${this.sessionOutput.toLocaleString()} out`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Get per-turn report for the last API call. */
|
|
102
|
-
getPerTurnReport(): string {
|
|
103
|
-
return `${this.lastTurnInput.toLocaleString()} in / ${this.lastTurnOutput.toLocaleString()} out`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Get all-time total report. */
|
|
107
|
-
getTotalReport(): string {
|
|
108
|
-
return `${this.totalInput.toLocaleString()} in / ${this.totalOutput.toLocaleString()} out`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Get turn count. */
|
|
112
|
-
getTurnCount(): number {
|
|
113
|
-
return this.turnCount;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Get snapshot for display / persistence. */
|
|
117
|
-
getSnapshot(): CostSnapshot {
|
|
118
|
-
return {
|
|
119
|
-
sessionInput: this.sessionInput,
|
|
120
|
-
sessionOutput: this.sessionOutput,
|
|
121
|
-
totalInput: this.totalInput,
|
|
122
|
-
totalOutput: this.totalOutput,
|
|
123
|
-
sessionCost: this.getSessionCost(),
|
|
124
|
-
totalCost: this.getTotalCost(),
|
|
125
|
-
lastUpdated: new Date().toISOString(),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Persist to costs.json. */
|
|
130
|
-
async save(): Promise<void> {
|
|
131
|
-
const snapshot = this.getSnapshot();
|
|
132
|
-
await fsp.writeFile(this.costFile, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
133
|
-
}
|
|
134
|
-
}
|
package/src/engine/gatekeeper.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { Channel, ApprovalRequest } from 'memoryblock';
|
|
2
|
-
import { log } from 'memoryblock';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Gatekeeper: The sovereign human approval system.
|
|
6
|
-
* When a tool requires approval (e.g., shell commands), execution pauses
|
|
7
|
-
* and the system requests explicit human approval via the active channel.
|
|
8
|
-
*/
|
|
9
|
-
export class Gatekeeper {
|
|
10
|
-
private channel: Channel;
|
|
11
|
-
private blockName: string;
|
|
12
|
-
private monitorName: string;
|
|
13
|
-
|
|
14
|
-
constructor(channel: Channel, blockName: string, monitorName: string) {
|
|
15
|
-
this.channel = channel;
|
|
16
|
-
this.blockName = blockName;
|
|
17
|
-
this.monitorName = monitorName;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Request human approval for a tool execution.
|
|
22
|
-
* Returns true if approved, false if denied.
|
|
23
|
-
*/
|
|
24
|
-
async requestApproval(
|
|
25
|
-
toolName: string,
|
|
26
|
-
toolInput: Record<string, unknown>,
|
|
27
|
-
): Promise<boolean> {
|
|
28
|
-
const description = this.formatDescription(toolName, toolInput);
|
|
29
|
-
|
|
30
|
-
const request: ApprovalRequest = {
|
|
31
|
-
toolName,
|
|
32
|
-
toolInput,
|
|
33
|
-
description,
|
|
34
|
-
blockName: this.blockName,
|
|
35
|
-
monitorName: this.monitorName,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
log.system(this.blockName, `Approval required: ${description}`);
|
|
39
|
-
|
|
40
|
-
return this.channel.requestApproval(request);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Format a human-readable description of the action. */
|
|
44
|
-
private formatDescription(toolName: string, input: Record<string, unknown>): string {
|
|
45
|
-
if (toolName === 'execute_command') {
|
|
46
|
-
return `Run command: ${input.command}`;
|
|
47
|
-
}
|
|
48
|
-
const params = Object.entries(input)
|
|
49
|
-
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
50
|
-
.join(', ');
|
|
51
|
-
return `${toolName}(${params})`;
|
|
52
|
-
}
|
|
53
|
-
}
|