opc-agent 4.1.1 → 4.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/src/doctor.ts CHANGED
@@ -1,156 +1,243 @@
1
- import { execSync } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import * as net from 'net';
4
-
5
- export interface CheckResult {
6
- ok: boolean;
7
- detail: string;
8
- fix?: string;
9
- }
10
-
11
- export interface DoctorCheck {
12
- name: string;
13
- check: () => CheckResult | Promise<CheckResult>;
14
- }
15
-
16
- export function getDoctorChecks(): DoctorCheck[] {
17
- return [
18
- {
19
- name: 'Node.js version',
20
- check: () => {
21
- const v = process.versions.node.split('.').map(Number);
22
- return {
23
- ok: v[0] >= 18,
24
- detail: `v${process.versions.node}`,
25
- fix: v[0] < 18 ? 'Upgrade to Node 18+: https://nodejs.org' : undefined,
26
- };
27
- },
28
- },
29
- {
30
- name: 'npm version',
31
- check: () => {
32
- try {
33
- const v = execSync('npm --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
34
- return { ok: true, detail: `v${v}` };
35
- } catch {
36
- return { ok: false, detail: 'Not found', fix: 'Install npm: https://nodejs.org' };
37
- }
38
- },
39
- },
40
- {
41
- name: 'Ollama running',
42
- check: async () => {
43
- try {
44
- const controller = new AbortController();
45
- const timeout = setTimeout(() => controller.abort(), 3000);
46
- const r = await fetch('http://localhost:11434/api/tags', { signal: controller.signal });
47
- clearTimeout(timeout);
48
- const data = await r.json() as any;
49
- return { ok: true, detail: `${data.models?.length || 0} models available` };
50
- } catch {
51
- return { ok: false, detail: 'Not running', fix: 'Install Ollama: https://ollama.ai' };
52
- }
53
- },
54
- },
55
- {
56
- name: 'agent.yaml exists',
57
- check: () => {
58
- const found = existsSync('./agent.yaml');
59
- return { ok: found, detail: found ? 'Found' : 'Not found', fix: found ? undefined : 'Run `opc init` to create a project' };
60
- },
61
- },
62
- {
63
- name: 'SOUL.md exists',
64
- check: () => {
65
- const found = existsSync('./SOUL.md');
66
- return { ok: found, detail: found ? 'Found' : 'Not found', fix: found ? undefined : 'Run `opc init` to generate one' };
67
- },
68
- },
69
- {
70
- name: 'TypeScript installed',
71
- check: () => {
72
- try {
73
- execSync('npx tsc --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
74
- return { ok: true, detail: 'Available' };
75
- } catch {
76
- return { ok: false, detail: 'Not found', fix: 'npm install -D typescript' };
77
- }
78
- },
79
- },
80
- {
81
- name: 'Disk space',
82
- check: () => {
83
- return { ok: true, detail: 'Check passed' };
84
- },
85
- },
86
- {
87
- name: 'DeepBrain package',
88
- check: () => {
89
- try {
90
- require.resolve('deepbrain');
91
- return { ok: true, detail: 'Installed' };
92
- } catch {
93
- return { ok: false, detail: 'Not installed', fix: 'npm install deepbrain' };
94
- }
95
- },
96
- },
97
- {
98
- name: 'Port 3000 available',
99
- check: () => {
100
- return new Promise<CheckResult>((resolve) => {
101
- const server = net.createServer();
102
- server.once('error', () => {
103
- resolve({ ok: false, detail: 'In use', fix: 'Free port 3000 or configure a different port' });
104
- });
105
- server.once('listening', () => {
106
- server.close(() => {
107
- resolve({ ok: true, detail: 'Available' });
108
- });
109
- });
110
- server.listen(3000);
111
- });
112
- },
113
- },
114
- ];
115
- }
116
-
117
- export async function runDoctor(): Promise<{ passed: number; total: number }> {
118
- const checks = getDoctorChecks();
119
- const color = {
120
- green: (s: string) => `\x1b[32m${s}\x1b[0m`,
121
- red: (s: string) => `\x1b[31m${s}\x1b[0m`,
122
- dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
123
- bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
124
- };
125
-
126
- console.log(`\n🔍 ${color.bold('OPC Agent Doctor')}\n`);
127
-
128
- let passed = 0;
129
- const total = checks.length;
130
-
131
- for (const check of checks) {
132
- try {
133
- const result = await check.check();
134
- const icon = result.ok ? color.green('✅') : color.red('❌');
135
- const name = check.name.padEnd(22);
136
- console.log(` ${icon} ${name} ${result.detail}`);
137
- if (!result.ok && result.fix) {
138
- console.log(` → ${result.fix}`);
139
- }
140
- if (result.ok) passed++;
141
- } catch (err) {
142
- const name = check.name.padEnd(22);
143
- console.log(` ${color.red('❌')} ${name} Error: ${err instanceof Error ? err.message : String(err)}`);
144
- }
145
- }
146
-
147
- console.log(`\n Result: ${passed}/${total} checks passed`);
148
- if (passed < total) {
149
- console.log(`\n Fix the issues above to get the best experience.`);
150
- } else {
151
- console.log(`\n ${color.green('All checks passed!')} You're good to go.`);
152
- }
153
- console.log();
154
-
155
- return { passed, total };
156
- }
1
+ import { execSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import * as net from 'net';
4
+ import * as yaml from 'js-yaml';
5
+
6
+ export interface CheckResult {
7
+ ok: boolean;
8
+ detail: string;
9
+ fix?: string;
10
+ optional?: boolean; // ⚠️ 而不是 ❌
11
+ }
12
+
13
+ export interface DoctorCheck {
14
+ name: string;
15
+ check: () => CheckResult | Promise<CheckResult>;
16
+ }
17
+
18
+ /** 读取 .env 文件并解析为 key-value */
19
+ function loadEnvFile(): Record<string, string> {
20
+ const envPath = '.env';
21
+ if (!existsSync(envPath)) return {};
22
+ const result: Record<string, string> = {};
23
+ try {
24
+ const content = readFileSync(envPath, 'utf-8');
25
+ for (const line of content.split('\n')) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith('#')) continue;
28
+ const eqIdx = trimmed.indexOf('=');
29
+ if (eqIdx === -1) continue;
30
+ result[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
31
+ }
32
+ } catch { /* ignore */ }
33
+ return result;
34
+ }
35
+
36
+ /** oad.yaml 读取 provider 配置 */
37
+ function loadOadProvider(): string | undefined {
38
+ for (const f of ['oad.yaml', 'agent.yaml']) {
39
+ if (existsSync(f)) {
40
+ try {
41
+ const cfg = yaml.load(readFileSync(f, 'utf-8')) as any;
42
+ return cfg?.spec?.provider?.default;
43
+ } catch { /* ignore */ }
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ export function getDoctorChecks(): DoctorCheck[] {
50
+ return [
51
+ {
52
+ name: 'Node.js version',
53
+ check: () => {
54
+ const v = process.versions.node.split('.').map(Number);
55
+ return {
56
+ ok: v[0] >= 18,
57
+ detail: `v${process.versions.node}`,
58
+ fix: v[0] < 18 ? 'Upgrade to Node 18+: https://nodejs.org' : undefined,
59
+ };
60
+ },
61
+ },
62
+ {
63
+ name: 'npm version',
64
+ check: () => {
65
+ try {
66
+ const v = execSync('npm --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
67
+ return { ok: true, detail: `v${v}` };
68
+ } catch {
69
+ return { ok: false, detail: 'Not found', fix: 'Install npm: https://nodejs.org' };
70
+ }
71
+ },
72
+ },
73
+ {
74
+ // Ollama 是可选的(只有选了 ollama provider 才需要)
75
+ name: 'Ollama running',
76
+ check: async () => {
77
+ try {
78
+ const controller = new AbortController();
79
+ const timeout = setTimeout(() => controller.abort(), 3000);
80
+ const r = await fetch('http://localhost:11434/api/tags', { signal: controller.signal });
81
+ clearTimeout(timeout);
82
+ const data = await r.json() as any;
83
+ return { ok: true, detail: `${data.models?.length || 0} models available` };
84
+ } catch {
85
+ return { ok: false, detail: 'Not running', fix: 'Install Ollama: https://ollama.ai (optional, only needed for local models)', optional: true };
86
+ }
87
+ },
88
+ },
89
+ {
90
+ // 检查 oad.yaml 而不是 agent.yaml
91
+ name: 'oad.yaml exists',
92
+ check: () => {
93
+ const found = existsSync('./oad.yaml');
94
+ if (found) return { ok: true, detail: 'Found' };
95
+ // 检查是否有旧的 agent.yaml 需要迁移
96
+ if (existsSync('./agent.yaml')) {
97
+ return { ok: false, detail: 'Not found (found agent.yaml)', fix: 'Run `opc migrate` to migrate agent.yaml → oad.yaml' };
98
+ }
99
+ return { ok: false, detail: 'Not found', fix: 'Run `opc init` to create a project' };
100
+ },
101
+ },
102
+ {
103
+ name: 'SOUL.md exists',
104
+ check: () => {
105
+ const found = existsSync('./SOUL.md');
106
+ return { ok: found, detail: found ? 'Found' : 'Not found', fix: found ? undefined : 'Run `opc init` to generate one' };
107
+ },
108
+ },
109
+ {
110
+ // TypeScript 是可选的
111
+ name: 'TypeScript installed',
112
+ check: () => {
113
+ try {
114
+ execSync('npx tsc --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
115
+ return { ok: true, detail: 'Available' };
116
+ } catch {
117
+ return { ok: false, detail: 'Not found', fix: 'npm install -D typescript (optional)', optional: true };
118
+ }
119
+ },
120
+ },
121
+ {
122
+ name: 'Disk space',
123
+ check: () => {
124
+ return { ok: true, detail: 'Check passed' };
125
+ },
126
+ },
127
+ {
128
+ // DeepBrain 是可选的
129
+ name: 'DeepBrain package',
130
+ check: () => {
131
+ try {
132
+ require.resolve('deepbrain');
133
+ return { ok: true, detail: 'Installed' };
134
+ } catch {
135
+ return { ok: false, detail: 'Not installed', fix: 'npm install deepbrain (optional, for long-term memory)', optional: true };
136
+ }
137
+ },
138
+ },
139
+ {
140
+ name: 'Port 3000 available',
141
+ check: () => {
142
+ return new Promise<CheckResult>((resolve) => {
143
+ const server = net.createServer();
144
+ server.once('error', () => {
145
+ resolve({ ok: false, detail: 'In use', fix: 'Free port 3000 or configure a different port' });
146
+ });
147
+ server.once('listening', () => {
148
+ server.close(() => {
149
+ resolve({ ok: true, detail: 'Available' });
150
+ });
151
+ });
152
+ server.listen(3000);
153
+ });
154
+ },
155
+ },
156
+ {
157
+ // 检查 API key 是否配置(不是占位符)
158
+ name: 'API key configured',
159
+ check: () => {
160
+ const env = loadEnvFile();
161
+ const apiKey = env['OPC_LLM_API_KEY'] || '';
162
+ const oadProvider = loadOadProvider();
163
+ // Ollama 不需要 API key
164
+ if (oadProvider === 'ollama') {
165
+ return { ok: true, detail: 'Not required (Ollama provider)' };
166
+ }
167
+ if (!apiKey || apiKey === 'your-api-key-here') {
168
+ return { ok: false, detail: 'Not configured or still placeholder', fix: 'Edit .env and set OPC_LLM_API_KEY to your actual API key' };
169
+ }
170
+ return { ok: true, detail: 'Configured' };
171
+ },
172
+ },
173
+ {
174
+ // 检查 .env 和 oad.yaml 的 provider 是否匹配
175
+ name: 'Provider consistency',
176
+ check: () => {
177
+ const env = loadEnvFile();
178
+ const baseUrl = env['OPC_LLM_BASE_URL'] || '';
179
+ const oadProvider = loadOadProvider();
180
+ if (!oadProvider || !baseUrl) {
181
+ return { ok: true, detail: 'N/A (no config to compare)' };
182
+ }
183
+ // 检测 .env 的 base URL 暗示的 provider
184
+ let envProvider = 'unknown';
185
+ if (baseUrl.includes('openai.com')) envProvider = 'openai';
186
+ else if (baseUrl.includes('deepseek.com')) envProvider = 'deepseek';
187
+ else if (baseUrl.includes('localhost:11434')) envProvider = 'ollama';
188
+ else if (baseUrl.includes('anthropic.com')) envProvider = 'anthropic';
189
+ else if (baseUrl.includes('dashscope.aliyuncs.com')) envProvider = 'qwen';
190
+
191
+ if (envProvider === 'unknown') return { ok: true, detail: `Custom base URL (${oadProvider})` };
192
+ if (envProvider !== oadProvider && oadProvider !== 'auto') {
193
+ return { ok: false, detail: `Mismatch: .env → ${envProvider}, oad.yaml → ${oadProvider}`, fix: 'Update .env or oad.yaml to use the same provider' };
194
+ }
195
+ return { ok: true, detail: `Matched: ${oadProvider}` };
196
+ },
197
+ },
198
+ ];
199
+ }
200
+
201
+ export async function runDoctor(): Promise<{ passed: number; total: number }> {
202
+ const checks = getDoctorChecks();
203
+ const color = {
204
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
205
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
206
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
207
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
208
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
209
+ };
210
+
211
+ console.log(`\n🔍 ${color.bold('OPC Agent Doctor')}\n`);
212
+
213
+ let passed = 0;
214
+ const total = checks.length;
215
+
216
+ for (const check of checks) {
217
+ try {
218
+ const result = await check.check();
219
+ // optional 项失败显示 ⚠️ 而不是 ❌
220
+ const icon = result.ok ? color.green('✅') : (result.optional ? color.yellow('⚠️') : color.red('❌'));
221
+ const name = check.name.padEnd(24);
222
+ console.log(` ${icon} ${name} ${result.detail}`);
223
+ if (!result.ok && result.fix) {
224
+ console.log(` → ${result.fix}`);
225
+ }
226
+ // optional 项即使失败也算 passed
227
+ if (result.ok || result.optional) passed++;
228
+ } catch (err) {
229
+ const name = check.name.padEnd(24);
230
+ console.log(` ${color.red('❌')} ${name} Error: ${err instanceof Error ? err.message : String(err)}`);
231
+ }
232
+ }
233
+
234
+ console.log(`\n Result: ${passed}/${total} checks passed`);
235
+ if (passed < total) {
236
+ console.log(`\n Fix the issues above to get the best experience.`);
237
+ } else {
238
+ console.log(`\n ${color.green('All checks passed!')} You're good to go.`);
239
+ }
240
+ console.log();
241
+
242
+ return { passed, total };
243
+ }
@@ -1,9 +1,103 @@
1
1
  import type { Message, MemoryStore } from '../core/types';
2
2
  import { InMemoryStore } from './index';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { join, resolve } from 'path';
5
+
6
+ /**
7
+ * 本地 JSON 文件持久化存储,作为 DeepBrain 不可用时的 fallback。
8
+ * 数据保存在 .opc/memory.json,进程重启后记忆不会丢失。
9
+ */
10
+ class FileBackedStore implements MemoryStore {
11
+ private store: Map<string, unknown> = new Map();
12
+ private conversations: Map<string, Message[]> = new Map();
13
+ private filePath: string;
14
+ private dirty = false;
15
+ private saveTimer: ReturnType<typeof setTimeout> | null = null;
16
+
17
+ constructor(baseDir: string = '.') {
18
+ const opcDir = join(resolve(baseDir), '.opc');
19
+ if (!existsSync(opcDir)) mkdirSync(opcDir, { recursive: true });
20
+ this.filePath = join(opcDir, 'memory.json');
21
+ this.loadFromFile();
22
+ }
23
+
24
+ private loadFromFile(): void {
25
+ if (!existsSync(this.filePath)) return;
26
+ try {
27
+ const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
28
+ if (data.store) {
29
+ for (const [k, v] of Object.entries(data.store)) {
30
+ this.store.set(k, v);
31
+ }
32
+ }
33
+ if (data.conversations) {
34
+ for (const [k, v] of Object.entries(data.conversations)) {
35
+ this.conversations.set(k, v as Message[]);
36
+ }
37
+ }
38
+ } catch { /* 文件损坏则忽略,从空开始 */ }
39
+ }
40
+
41
+ private scheduleSave(): void {
42
+ this.dirty = true;
43
+ if (this.saveTimer) return; // 已经有定时器在等了
44
+ // 延迟 1 秒批量写入,避免高频写磁盘
45
+ this.saveTimer = setTimeout(() => {
46
+ this.saveTimer = null;
47
+ if (this.dirty) this.saveToFile();
48
+ }, 1000);
49
+ }
50
+
51
+ private saveToFile(): void {
52
+ try {
53
+ const data = {
54
+ store: Object.fromEntries(this.store),
55
+ conversations: Object.fromEntries(this.conversations),
56
+ updatedAt: new Date().toISOString(),
57
+ };
58
+ writeFileSync(this.filePath, JSON.stringify(data, null, 2));
59
+ this.dirty = false;
60
+ } catch { /* 写入失败不影响运行 */ }
61
+ }
62
+
63
+ async get(key: string): Promise<unknown> {
64
+ return this.store.get(key);
65
+ }
66
+
67
+ async set(key: string, value: unknown): Promise<void> {
68
+ this.store.set(key, value);
69
+ this.scheduleSave();
70
+ }
71
+
72
+ async getConversation(sessionId: string): Promise<Message[]> {
73
+ return this.conversations.get(sessionId) ?? [];
74
+ }
75
+
76
+ async addMessage(sessionId: string, message: Message): Promise<void> {
77
+ if (!this.conversations.has(sessionId)) {
78
+ this.conversations.set(sessionId, []);
79
+ }
80
+ const conv = this.conversations.get(sessionId)!;
81
+ conv.push(message);
82
+ // 每个 session 最多保留 200 条消息,避免文件无限增长
83
+ if (conv.length > 200) conv.splice(0, conv.length - 200);
84
+ this.scheduleSave();
85
+ }
86
+
87
+ async clear(sessionId?: string): Promise<void> {
88
+ if (sessionId) {
89
+ this.conversations.delete(sessionId);
90
+ } else {
91
+ this.store.clear();
92
+ this.conversations.clear();
93
+ }
94
+ this.scheduleSave();
95
+ }
96
+ }
3
97
 
4
98
  /**
5
99
  * DeepBrain-backed memory store for long-term semantic memory.
6
- * Falls back to InMemoryStore if deepbrain package is not installed.
100
+ * Falls back to local JSON file storage (.opc/memory.json) if deepbrain package is not installed.
7
101
  */
8
102
  export interface DeepBrainClient {
9
103
  store(collection: string, id: string, content: string, metadata?: Record<string, unknown>): Promise<void>;
@@ -12,13 +106,13 @@ export interface DeepBrainClient {
12
106
  }
13
107
 
14
108
  export class DeepBrainMemoryStore implements MemoryStore {
15
- private fallback: InMemoryStore;
109
+ private fallback: FileBackedStore;
16
110
  private client: DeepBrainClient | null = null;
17
111
  private collection: string;
18
112
  private ready: Promise<boolean>;
19
113
 
20
114
  constructor(options: { collection?: string; config?: Record<string, unknown> } = {}) {
21
- this.fallback = new InMemoryStore();
115
+ this.fallback = new FileBackedStore();
22
116
  this.collection = options.collection ?? 'agent-memory';
23
117
  this.ready = this.initClient(options.config);
24
118
  }
@@ -29,12 +123,12 @@ export class DeepBrainMemoryStore implements MemoryStore {
29
123
  const deepbrain = await import(/* webpackIgnore: true */ 'deepbrain');
30
124
  this.client = (deepbrain as any).createClient?.(config) ?? (deepbrain as any).default?.createClient?.(config);
31
125
  if (!this.client) {
32
- console.warn('[DeepBrainMemory] Could not create client, using in-memory fallback');
126
+ console.warn('[DeepBrainMemory] Could not create client, using file-backed fallback (.opc/memory.json)');
33
127
  return false;
34
128
  }
35
129
  return true;
36
130
  } catch {
37
- console.warn('[DeepBrainMemory] deepbrain package not found, using in-memory fallback');
131
+ console.warn('[DeepBrainMemory] deepbrain package not found, using file-backed fallback (.opc/memory.json)');
38
132
  return false;
39
133
  }
40
134
  }
@@ -6,7 +6,6 @@
6
6
 
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
8
  import { join } from 'path';
9
- import * as os from 'os';
10
9
  import { parseCron, cronMatches, Scheduler } from '../core/scheduler';
11
10
  import type { CronJob, JobHandler } from '../core/scheduler';
12
11
 
@@ -29,8 +28,9 @@ export interface SchedulesStore {
29
28
  tasks: ScheduleTask[];
30
29
  }
31
30
 
32
- function getSchedulesPath(): string {
33
- const dir = join(os.homedir(), '.opc');
31
+ function getSchedulesPath(projectDir?: string): string {
32
+ // 使用项目本地路径而不是全局 ~/.opc/,避免新 agent 加载其他项目的任务
33
+ const dir = projectDir ? join(projectDir, '.opc') : join(process.cwd(), '.opc');
34
34
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
35
35
  return join(dir, 'schedules.json');
36
36
  }