opc-agent 3.0.0 → 4.0.0
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 +30 -24
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli.js +36 -0
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/index.d.ts +64 -1
- package/dist/index.js +86 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +6 -1
- package/dist/tools/builtin/index.js +18 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/package.json +3 -3
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli.ts +40 -0
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/index.ts +84 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +9 -2
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export interface IDEConfig {
|
|
4
|
+
editor: 'vscode' | 'jetbrains' | 'zed' | 'cursor';
|
|
5
|
+
workspacePath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Diagnostic {
|
|
9
|
+
path: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
severity: 'error' | 'warning' | 'info';
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TextEdit {
|
|
17
|
+
range: Range;
|
|
18
|
+
newText: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Range {
|
|
22
|
+
startLine: number;
|
|
23
|
+
startColumn: number;
|
|
24
|
+
endLine: number;
|
|
25
|
+
endColumn: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SearchOptions {
|
|
29
|
+
caseSensitive?: boolean;
|
|
30
|
+
regex?: boolean;
|
|
31
|
+
include?: string;
|
|
32
|
+
exclude?: string;
|
|
33
|
+
maxResults?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SearchResult {
|
|
37
|
+
path: string;
|
|
38
|
+
line: number;
|
|
39
|
+
column: number;
|
|
40
|
+
text: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class IDEBridge {
|
|
44
|
+
private config: IDEConfig;
|
|
45
|
+
|
|
46
|
+
constructor(config: IDEConfig) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private getCliCommand(): string {
|
|
51
|
+
switch (this.config.editor) {
|
|
52
|
+
case 'vscode': case 'cursor': return this.config.editor === 'cursor' ? 'cursor' : 'code';
|
|
53
|
+
case 'jetbrains': return 'idea';
|
|
54
|
+
case 'zed': return 'zed';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private exec(cmd: string): string {
|
|
59
|
+
try {
|
|
60
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 10000, cwd: this.config.workspacePath }).trim();
|
|
61
|
+
} catch (e: any) {
|
|
62
|
+
throw new Error(`IDE command failed: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async openFile(path: string, line?: number): Promise<void> {
|
|
67
|
+
const cli = this.getCliCommand();
|
|
68
|
+
const target = line ? `${path}:${line}` : path;
|
|
69
|
+
if (this.config.editor === 'vscode' || this.config.editor === 'cursor') {
|
|
70
|
+
this.exec(`${cli} --goto "${target}"`);
|
|
71
|
+
} else if (this.config.editor === 'zed') {
|
|
72
|
+
this.exec(`zed "${target}"`);
|
|
73
|
+
} else {
|
|
74
|
+
this.exec(`${cli} --line ${line || 1} "${path}"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getDiagnostics(path?: string): Promise<Diagnostic[]> {
|
|
79
|
+
// VS Code doesn't expose diagnostics via CLI; return empty as stub
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async runCommand(command: string): Promise<string> {
|
|
84
|
+
const cli = this.getCliCommand();
|
|
85
|
+
if (this.config.editor === 'vscode' || this.config.editor === 'cursor') {
|
|
86
|
+
return this.exec(`${cli} --command "${command}"`);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`runCommand not supported for ${this.config.editor}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getOpenFiles(): Promise<string[]> {
|
|
92
|
+
// Stub — no standard CLI to get open files
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async applyEdit(path: string, edits: TextEdit[]): Promise<void> {
|
|
97
|
+
// Stub — would use editor's API/extension
|
|
98
|
+
if (edits.length === 0) return;
|
|
99
|
+
throw new Error('applyEdit requires an IDE extension to be installed. Use file system edits as fallback.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
|
|
103
|
+
const cli = this.getCliCommand();
|
|
104
|
+
if (this.config.editor === 'vscode' || this.config.editor === 'cursor') {
|
|
105
|
+
try {
|
|
106
|
+
const args = [`--search "${query}"`];
|
|
107
|
+
if (options?.include) args.push(`--include "${options.include}"`);
|
|
108
|
+
const output = this.exec(`${cli} ${args.join(' ')}`);
|
|
109
|
+
// Parse output lines
|
|
110
|
+
return output.split('\n').filter(Boolean).map(line => {
|
|
111
|
+
const match = line.match(/^(.+):(\d+):(\d+):(.*)$/);
|
|
112
|
+
if (!match) return { path: '', line: 0, column: 0, text: line };
|
|
113
|
+
return { path: match[1], line: parseInt(match[2]), column: parseInt(match[3]), text: match[4] };
|
|
114
|
+
});
|
|
115
|
+
} catch { return []; }
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getSelection(): Promise<{ path: string; text: string; range: Range } | null> {
|
|
121
|
+
// Stub — requires editor extension
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async installExtension(extensionId: string): Promise<void> {
|
|
126
|
+
const cli = this.getCliCommand();
|
|
127
|
+
if (this.config.editor === 'vscode' || this.config.editor === 'cursor') {
|
|
128
|
+
this.exec(`${cli} --install-extension ${extensionId}`);
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(`installExtension not supported for ${this.config.editor}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
export interface RemoteNode {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
type: 'vps' | 'pi' | 'phone' | 'desktop';
|
|
10
|
+
status: 'online' | 'offline' | 'pairing';
|
|
11
|
+
capabilities: string[];
|
|
12
|
+
lastSeen: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class NodeNetwork extends EventEmitter {
|
|
16
|
+
private nodes = new Map<string, RemoteNode>();
|
|
17
|
+
|
|
18
|
+
addNode(config: Partial<RemoteNode>): RemoteNode {
|
|
19
|
+
const node: RemoteNode = {
|
|
20
|
+
id: config.id || randomUUID(),
|
|
21
|
+
name: config.name || 'unnamed',
|
|
22
|
+
host: config.host || 'localhost',
|
|
23
|
+
port: config.port || 8080,
|
|
24
|
+
type: config.type || 'desktop',
|
|
25
|
+
status: config.status || 'offline',
|
|
26
|
+
capabilities: config.capabilities || [],
|
|
27
|
+
lastSeen: Date.now(),
|
|
28
|
+
};
|
|
29
|
+
this.nodes.set(node.id, node);
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeNode(id: string): void {
|
|
34
|
+
if (!this.nodes.has(id)) throw new Error(`Node ${id} not found`);
|
|
35
|
+
this.nodes.delete(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
listNodes(): RemoteNode[] {
|
|
39
|
+
return Array.from(this.nodes.values());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getNode(id: string): RemoteNode | null {
|
|
43
|
+
return this.nodes.get(id) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async pair(nodeId: string, pairingCode: string): Promise<boolean> {
|
|
47
|
+
const node = this.nodes.get(nodeId);
|
|
48
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
49
|
+
if (!pairingCode || pairingCode.length < 4) return false;
|
|
50
|
+
node.status = 'online';
|
|
51
|
+
node.lastSeen = Date.now();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async sendCommand(nodeId: string, command: string): Promise<any> {
|
|
56
|
+
const node = this.nodes.get(nodeId);
|
|
57
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
58
|
+
if (node.status !== 'online') throw new Error(`Node ${nodeId} is ${node.status}`);
|
|
59
|
+
node.lastSeen = Date.now();
|
|
60
|
+
// Stub: return command echo
|
|
61
|
+
return { nodeId, command, status: 'sent', timestamp: Date.now() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async broadcast(command: string): Promise<Map<string, any>> {
|
|
65
|
+
const results = new Map<string, any>();
|
|
66
|
+
for (const [id, node] of this.nodes) {
|
|
67
|
+
if (node.status === 'online') {
|
|
68
|
+
try {
|
|
69
|
+
results.set(id, await this.sendCommand(id, command));
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
results.set(id, { error: e.message });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async healthCheck(): Promise<Map<string, boolean>> {
|
|
79
|
+
const results = new Map<string, boolean>();
|
|
80
|
+
for (const [id, node] of this.nodes) {
|
|
81
|
+
results.set(id, node.status === 'online');
|
|
82
|
+
node.lastSeen = Date.now();
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
// ─── Types ───────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface ProfileConfig {
|
|
8
|
+
model?: string;
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
temperature?: number;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Profile {
|
|
15
|
+
name: string;
|
|
16
|
+
config: ProfileConfig;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
lastUsed: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── ProfileManager ─────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export class ProfileManager {
|
|
24
|
+
private profilesDir: string;
|
|
25
|
+
private currentName: string = 'default';
|
|
26
|
+
|
|
27
|
+
constructor(baseDir?: string) {
|
|
28
|
+
this.profilesDir = baseDir ?? path.join(os.homedir(), '.opc', 'profiles');
|
|
29
|
+
this.ensureDir(this.profilesDir);
|
|
30
|
+
|
|
31
|
+
// Load current profile pointer
|
|
32
|
+
const pointerFile = path.join(this.profilesDir, '.current');
|
|
33
|
+
if (fs.existsSync(pointerFile)) {
|
|
34
|
+
this.currentName = fs.readFileSync(pointerFile, 'utf-8').trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private ensureDir(dir: string): void {
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private profileDir(name: string): string {
|
|
45
|
+
return path.join(this.profilesDir, name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private profileFile(name: string): string {
|
|
49
|
+
return path.join(this.profileDir(name), 'profile.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
create(name: string, config: ProfileConfig = {}): Profile {
|
|
53
|
+
const dir = this.profileDir(name);
|
|
54
|
+
if (fs.existsSync(dir)) {
|
|
55
|
+
throw new Error(`Profile '${name}' already exists`);
|
|
56
|
+
}
|
|
57
|
+
this.ensureDir(dir);
|
|
58
|
+
// Also create memory and sessions dirs
|
|
59
|
+
this.ensureDir(path.join(dir, 'memory'));
|
|
60
|
+
this.ensureDir(path.join(dir, 'sessions'));
|
|
61
|
+
|
|
62
|
+
const profile: Profile = {
|
|
63
|
+
name,
|
|
64
|
+
config,
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
lastUsed: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
fs.writeFileSync(this.profileFile(name), JSON.stringify(profile, null, 2));
|
|
69
|
+
return profile;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch(name: string): void {
|
|
73
|
+
if (!fs.existsSync(this.profileDir(name))) {
|
|
74
|
+
throw new Error(`Profile '${name}' does not exist`);
|
|
75
|
+
}
|
|
76
|
+
this.currentName = name;
|
|
77
|
+
fs.writeFileSync(path.join(this.profilesDir, '.current'), name);
|
|
78
|
+
|
|
79
|
+
// Update lastUsed
|
|
80
|
+
const profile = this.get(name);
|
|
81
|
+
profile.lastUsed = Date.now();
|
|
82
|
+
fs.writeFileSync(this.profileFile(name), JSON.stringify(profile, null, 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
list(): Profile[] {
|
|
86
|
+
if (!fs.existsSync(this.profilesDir)) return [];
|
|
87
|
+
return fs.readdirSync(this.profilesDir)
|
|
88
|
+
.filter(f => {
|
|
89
|
+
const full = path.join(this.profilesDir, f);
|
|
90
|
+
return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'profile.json'));
|
|
91
|
+
})
|
|
92
|
+
.map(f => this.get(f));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get(name: string): Profile {
|
|
96
|
+
const file = this.profileFile(name);
|
|
97
|
+
if (!fs.existsSync(file)) {
|
|
98
|
+
throw new Error(`Profile '${name}' not found`);
|
|
99
|
+
}
|
|
100
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
delete(name: string): void {
|
|
104
|
+
if (name === this.currentName) {
|
|
105
|
+
throw new Error(`Cannot delete the current active profile '${name}'`);
|
|
106
|
+
}
|
|
107
|
+
const dir = this.profileDir(name);
|
|
108
|
+
if (!fs.existsSync(dir)) {
|
|
109
|
+
throw new Error(`Profile '${name}' does not exist`);
|
|
110
|
+
}
|
|
111
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
current(): Profile {
|
|
115
|
+
try {
|
|
116
|
+
return this.get(this.currentName);
|
|
117
|
+
} catch {
|
|
118
|
+
// Auto-create default
|
|
119
|
+
return this.create(this.currentName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/core/sandbox.ts
CHANGED
|
@@ -242,3 +242,103 @@ export class Sandbox {
|
|
|
242
242
|
return this.maxFiles;
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
// ─── Remote Sandbox Manager (v2.2.0) ────────────────────────
|
|
247
|
+
|
|
248
|
+
export interface RemoteSandboxConfig {
|
|
249
|
+
backend: 'local' | 'docker' | 'ssh';
|
|
250
|
+
docker?: { image: string; volumes?: string[] };
|
|
251
|
+
ssh?: { host: string; user: string; keyPath?: string };
|
|
252
|
+
timeout?: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface ExecResult {
|
|
256
|
+
stdout: string;
|
|
257
|
+
stderr: string;
|
|
258
|
+
exitCode: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export class SandboxManager {
|
|
262
|
+
private defaultConfig: RemoteSandboxConfig;
|
|
263
|
+
|
|
264
|
+
constructor(config?: Partial<RemoteSandboxConfig>) {
|
|
265
|
+
this.defaultConfig = {
|
|
266
|
+
backend: config?.backend ?? 'local',
|
|
267
|
+
docker: config?.docker,
|
|
268
|
+
ssh: config?.ssh,
|
|
269
|
+
timeout: config?.timeout ?? 30000,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async exec(command: string, config?: Partial<RemoteSandboxConfig>): Promise<ExecResult> {
|
|
274
|
+
const cfg = { ...this.defaultConfig, ...config };
|
|
275
|
+
const { execSync } = await import('child_process');
|
|
276
|
+
|
|
277
|
+
switch (cfg.backend) {
|
|
278
|
+
case 'local': {
|
|
279
|
+
try {
|
|
280
|
+
const stdout = execSync(command, {
|
|
281
|
+
timeout: cfg.timeout,
|
|
282
|
+
encoding: 'utf-8',
|
|
283
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
284
|
+
});
|
|
285
|
+
return { stdout: stdout ?? '', stderr: '', exitCode: 0 };
|
|
286
|
+
} catch (err: any) {
|
|
287
|
+
return {
|
|
288
|
+
stdout: err.stdout ?? '',
|
|
289
|
+
stderr: err.stderr ?? '',
|
|
290
|
+
exitCode: err.status ?? 1,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
case 'docker': {
|
|
295
|
+
if (!cfg.docker?.image) throw new Error('Docker image is required');
|
|
296
|
+
const volumes = (cfg.docker.volumes ?? []).map(v => `-v ${v}`).join(' ');
|
|
297
|
+
const dockerCmd = `docker run --rm ${volumes} ${cfg.docker.image} sh -c "${command.replace(/"/g, '\\"')}"`;
|
|
298
|
+
return this.exec(dockerCmd, { backend: 'local', timeout: cfg.timeout });
|
|
299
|
+
}
|
|
300
|
+
case 'ssh': {
|
|
301
|
+
if (!cfg.ssh?.host || !cfg.ssh?.user) throw new Error('SSH host and user are required');
|
|
302
|
+
const keyArg = cfg.ssh.keyPath ? `-i ${cfg.ssh.keyPath}` : '';
|
|
303
|
+
const sshCmd = `ssh ${keyArg} ${cfg.ssh.user}@${cfg.ssh.host} "${command.replace(/"/g, '\\"')}"`;
|
|
304
|
+
return this.exec(sshCmd, { backend: 'local', timeout: cfg.timeout });
|
|
305
|
+
}
|
|
306
|
+
default:
|
|
307
|
+
throw new Error(`Unknown sandbox backend: ${cfg.backend}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async upload(localPath: string, remotePath: string, config?: Partial<RemoteSandboxConfig>): Promise<void> {
|
|
312
|
+
const cfg = { ...this.defaultConfig, ...config };
|
|
313
|
+
if (cfg.backend === 'local') {
|
|
314
|
+
const fsp = await import('fs');
|
|
315
|
+
fsp.copyFileSync(localPath, remotePath);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (cfg.backend === 'ssh') {
|
|
319
|
+
const keyArg = cfg.ssh?.keyPath ? `-i ${cfg.ssh.keyPath}` : '';
|
|
320
|
+
await this.exec(`scp ${keyArg} "${localPath}" ${cfg.ssh!.user}@${cfg.ssh!.host}:"${remotePath}"`, { backend: 'local' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (cfg.backend === 'docker') {
|
|
324
|
+
throw new Error('Upload to docker not yet supported. Use volumes instead.');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async download(remotePath: string, localPath: string, config?: Partial<RemoteSandboxConfig>): Promise<void> {
|
|
329
|
+
const cfg = { ...this.defaultConfig, ...config };
|
|
330
|
+
if (cfg.backend === 'local') {
|
|
331
|
+
const fsp = await import('fs');
|
|
332
|
+
fsp.copyFileSync(remotePath, localPath);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (cfg.backend === 'ssh') {
|
|
336
|
+
const keyArg = cfg.ssh?.keyPath ? `-i ${cfg.ssh.keyPath}` : '';
|
|
337
|
+
await this.exec(`scp ${keyArg} ${cfg.ssh!.user}@${cfg.ssh!.host}:"${remotePath}" "${localPath}"`, { backend: 'local' });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (cfg.backend === 'docker') {
|
|
341
|
+
throw new Error('Download from docker not yet supported. Use volumes instead.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import type { Message } from './types';
|
|
5
|
+
|
|
6
|
+
export interface Session {
|
|
7
|
+
id: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
channel: string;
|
|
10
|
+
messages: Message[];
|
|
11
|
+
metadata: Record<string, any>;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
lastActivity: number;
|
|
14
|
+
parentId?: string;
|
|
15
|
+
compactedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SessionManager {
|
|
19
|
+
private sessions: Map<string, Session> = new Map();
|
|
20
|
+
private storageDir: string;
|
|
21
|
+
|
|
22
|
+
constructor(storageDir?: string) {
|
|
23
|
+
this.storageDir = storageDir || path.join(process.env.HOME || process.env.USERPROFILE || '~', '.opc', 'sessions');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
create(agentId: string, channel: string, parentId?: string): Session {
|
|
27
|
+
const session: Session = {
|
|
28
|
+
id: crypto.randomUUID(),
|
|
29
|
+
agentId,
|
|
30
|
+
channel,
|
|
31
|
+
messages: [],
|
|
32
|
+
metadata: {},
|
|
33
|
+
createdAt: Date.now(),
|
|
34
|
+
lastActivity: Date.now(),
|
|
35
|
+
parentId,
|
|
36
|
+
};
|
|
37
|
+
this.sessions.set(session.id, session);
|
|
38
|
+
return session;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(id: string): Session | null {
|
|
42
|
+
return this.sessions.get(id) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
list(filter?: { agentId?: string; channel?: string; active?: boolean }): Session[] {
|
|
46
|
+
let result = Array.from(this.sessions.values());
|
|
47
|
+
if (filter?.agentId) result = result.filter(s => s.agentId === filter.agentId);
|
|
48
|
+
if (filter?.channel) result = result.filter(s => s.channel === filter.channel);
|
|
49
|
+
if (filter?.active !== undefined) {
|
|
50
|
+
const cutoff = Date.now() - 30 * 60 * 1000; // 30 min
|
|
51
|
+
result = result.filter(s => filter.active ? s.lastActivity > cutoff : s.lastActivity <= cutoff);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addMessage(sessionId: string, message: Message): void {
|
|
57
|
+
const session = this.sessions.get(sessionId);
|
|
58
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
59
|
+
session.messages.push(message);
|
|
60
|
+
session.lastActivity = Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async compact(sessionId: string, brain?: any): Promise<void> {
|
|
64
|
+
const session = this.sessions.get(sessionId);
|
|
65
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
66
|
+
if (brain && typeof brain.compress === 'function') {
|
|
67
|
+
const compressed = await brain.compress(session.messages);
|
|
68
|
+
session.messages = [{ id: 'compacted', role: 'system', content: compressed, timestamp: Date.now() }];
|
|
69
|
+
} else {
|
|
70
|
+
// Simple: keep first and last 5 messages
|
|
71
|
+
if (session.messages.length > 10) {
|
|
72
|
+
const first = session.messages.slice(0, 2);
|
|
73
|
+
const last = session.messages.slice(-5);
|
|
74
|
+
session.messages = [...first, { id: 'compacted', role: 'system', content: `[${session.messages.length - 7} messages compacted]`, timestamp: Date.now() }, ...last];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
session.compactedAt = Date.now();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
prune(maxAge: number): number {
|
|
81
|
+
const cutoff = Date.now() - maxAge;
|
|
82
|
+
let pruned = 0;
|
|
83
|
+
for (const [id, session] of this.sessions) {
|
|
84
|
+
if (session.lastActivity < cutoff) {
|
|
85
|
+
this.sessions.delete(id);
|
|
86
|
+
pruned++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return pruned;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getLineage(sessionId: string): Session[] {
|
|
93
|
+
const lineage: Session[] = [];
|
|
94
|
+
let current = this.sessions.get(sessionId);
|
|
95
|
+
while (current) {
|
|
96
|
+
lineage.unshift(current);
|
|
97
|
+
current = current.parentId ? this.sessions.get(current.parentId) || undefined : undefined;
|
|
98
|
+
}
|
|
99
|
+
return lineage;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fork(sessionId: string): Session {
|
|
103
|
+
const parent = this.sessions.get(sessionId);
|
|
104
|
+
if (!parent) throw new Error(`Session ${sessionId} not found`);
|
|
105
|
+
return this.create(parent.agentId, parent.channel, parent.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export(sessionId: string): string {
|
|
109
|
+
const session = this.sessions.get(sessionId);
|
|
110
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
111
|
+
const lines = [`# Session ${session.id}`, `Agent: ${session.agentId} | Channel: ${session.channel}`, `Created: ${new Date(session.createdAt).toISOString()}`, ''];
|
|
112
|
+
for (const msg of session.messages) {
|
|
113
|
+
lines.push(`**${msg.role}** (${new Date(msg.timestamp).toISOString()}):`);
|
|
114
|
+
lines.push(msg.content);
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
save(): void {
|
|
121
|
+
if (!fs.existsSync(this.storageDir)) {
|
|
122
|
+
fs.mkdirSync(this.storageDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
for (const [id, session] of this.sessions) {
|
|
125
|
+
fs.writeFileSync(path.join(this.storageDir, `${id}.json`), JSON.stringify(session, null, 2));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
load(): void {
|
|
130
|
+
if (!fs.existsSync(this.storageDir)) return;
|
|
131
|
+
const files = fs.readdirSync(this.storageDir).filter(f => f.endsWith('.json'));
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const data = JSON.parse(fs.readFileSync(path.join(this.storageDir, file), 'utf-8'));
|
|
134
|
+
this.sessions.set(data.id, data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|