opc-agent 4.0.0 → 4.0.1

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.
Files changed (75) hide show
  1. package/README.md +404 -80
  2. package/README.zh-CN.md +82 -0
  3. package/dist/cli/chat.d.ts +2 -0
  4. package/dist/cli/chat.js +134 -0
  5. package/dist/cli/setup.d.ts +4 -0
  6. package/dist/cli/setup.js +303 -0
  7. package/dist/cli.js +106 -6
  8. package/dist/hub/brain-seed.d.ts +14 -0
  9. package/dist/hub/brain-seed.js +77 -0
  10. package/dist/hub/client.d.ts +25 -0
  11. package/dist/hub/client.js +44 -0
  12. package/dist/index.d.ts +4 -2
  13. package/dist/index.js +12 -3
  14. package/dist/providers/index.d.ts +1 -1
  15. package/dist/providers/index.js +54 -1
  16. package/dist/scheduler/cron-engine.d.ts +41 -0
  17. package/dist/scheduler/cron-engine.js +200 -0
  18. package/dist/scheduler/index.d.ts +3 -0
  19. package/dist/scheduler/index.js +7 -0
  20. package/dist/skills/builtin/index.d.ts +6 -0
  21. package/dist/skills/builtin/index.js +402 -0
  22. package/dist/skills/marketplace.d.ts +30 -0
  23. package/dist/skills/marketplace.js +142 -0
  24. package/dist/skills/types.d.ts +34 -0
  25. package/dist/skills/types.js +16 -0
  26. package/dist/studio/server.d.ts +25 -0
  27. package/dist/studio/server.js +780 -0
  28. package/dist/studio/templates-data.d.ts +21 -0
  29. package/dist/studio/templates-data.js +148 -0
  30. package/dist/studio-ui/index.html +2502 -1073
  31. package/dist/tools/builtin/index.d.ts +1 -0
  32. package/dist/tools/builtin/index.js +7 -2
  33. package/dist/tools/builtin/web-search.d.ts +9 -0
  34. package/dist/tools/builtin/web-search.js +150 -0
  35. package/dist/tools/document-processor.d.ts +39 -0
  36. package/dist/tools/document-processor.js +188 -0
  37. package/dist/tools/image-generator.d.ts +42 -0
  38. package/dist/tools/image-generator.js +136 -0
  39. package/dist/tools/web-scraper.d.ts +20 -0
  40. package/dist/tools/web-scraper.js +148 -0
  41. package/dist/tools/web-search.d.ts +51 -0
  42. package/dist/tools/web-search.js +152 -0
  43. package/install.ps1 +154 -0
  44. package/install.sh +164 -0
  45. package/package.json +63 -52
  46. package/src/cli/chat.ts +99 -0
  47. package/src/cli/setup.ts +314 -0
  48. package/src/cli.ts +108 -6
  49. package/src/hub/brain-seed.ts +54 -0
  50. package/src/hub/client.ts +60 -0
  51. package/src/index.ts +4 -2
  52. package/src/providers/index.ts +64 -1
  53. package/src/scheduler/cron-engine.ts +191 -0
  54. package/src/scheduler/index.ts +2 -0
  55. package/src/skills/builtin/index.ts +408 -0
  56. package/src/skills/marketplace.ts +113 -0
  57. package/src/skills/types.ts +42 -0
  58. package/src/studio/server.ts +1591 -791
  59. package/src/studio/templates-data.ts +178 -0
  60. package/src/studio-ui/index.html +2502 -1073
  61. package/src/tools/builtin/index.ts +37 -35
  62. package/src/tools/builtin/web-search.ts +126 -0
  63. package/src/tools/document-processor.ts +213 -0
  64. package/src/tools/image-generator.ts +150 -0
  65. package/src/tools/web-scraper.ts +179 -0
  66. package/src/tools/web-search.ts +180 -0
  67. package/tests/cron-engine.test.ts +101 -0
  68. package/tests/document-processor.test.ts +69 -0
  69. package/tests/e2e-nocode.test.ts +442 -0
  70. package/tests/image-generator.test.ts +84 -0
  71. package/tests/settings-api.test.ts +148 -0
  72. package/tests/setup.test.ts +73 -0
  73. package/tests/studio.test.ts +402 -229
  74. package/tests/voice-interaction.test.ts +38 -0
  75. package/tests/web-search.test.ts +155 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Brain-seed downloader and auto-learner.
3
+ * Downloads brain-seed files from Hub and optionally imports into DeepBrain.
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import type { HubBrainSeed } from './client';
9
+
10
+ export interface BrainSeedResult {
11
+ savedFiles: string[];
12
+ learnedCount: number;
13
+ }
14
+
15
+ /**
16
+ * Save brain-seed files to disk and optionally auto-learn into DeepBrain.
17
+ */
18
+ export async function downloadAndLearnBrainSeeds(
19
+ projectDir: string,
20
+ seeds: HubBrainSeed[],
21
+ ): Promise<BrainSeedResult> {
22
+ if (!seeds || seeds.length === 0) {
23
+ return { savedFiles: [], learnedCount: 0 };
24
+ }
25
+
26
+ const seedDir = path.join(projectDir, 'brain-seed');
27
+ fs.mkdirSync(seedDir, { recursive: true });
28
+
29
+ const savedFiles: string[] = [];
30
+ for (const seed of seeds) {
31
+ const filePath = path.join(seedDir, seed.filename);
32
+ fs.writeFileSync(filePath, seed.content, 'utf-8');
33
+ savedFiles.push(seed.filename);
34
+ }
35
+
36
+ // Try auto-learn into DeepBrain (optional dependency)
37
+ let learnedCount = 0;
38
+ try {
39
+ const { Brain } = require('deepbrain');
40
+ const brain = new Brain({ database: path.join(projectDir, 'data', 'brain.db') });
41
+ for (const seed of seeds) {
42
+ await brain.learn(seed.content, {
43
+ slug: `brain-seed/${seed.filename.replace(/\.md$/, '')}`,
44
+ title: `Brain Seed: ${seed.tier}`,
45
+ namespace: `seed/${seed.tier}`,
46
+ });
47
+ learnedCount++;
48
+ }
49
+ } catch {
50
+ // deepbrain not installed — that's fine, files are saved
51
+ }
52
+
53
+ return { savedFiles, learnedCount };
54
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Workstation Hub API client
3
+ * Fetches agent templates and brain-seed knowledge from the Hub.
4
+ */
5
+
6
+ const HUB_URL = process.env.OPC_HUB_URL || 'https://hub.deepleaper.com';
7
+ const HUB_TIMEOUT = 5000;
8
+
9
+ export interface HubTemplate {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ category: string;
14
+ tags?: string[];
15
+ }
16
+
17
+ export interface HubTemplateDetail extends HubTemplate {
18
+ files: Record<string, string>;
19
+ brainSeeds: HubBrainSeed[];
20
+ }
21
+
22
+ export interface HubBrainSeed {
23
+ filename: string;
24
+ content: string;
25
+ tier: 'industry' | 'job' | 'workstation';
26
+ }
27
+
28
+ async function hubFetch<T>(path: string): Promise<T> {
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), HUB_TIMEOUT);
31
+ try {
32
+ const res = await fetch(`${HUB_URL}${path}`, {
33
+ signal: controller.signal,
34
+ headers: { 'Accept': 'application/json', 'User-Agent': 'opc-agent/2.0' },
35
+ });
36
+ if (!res.ok) throw new Error(`Hub API ${res.status} ${res.statusText}`);
37
+ return (await res.json()) as T;
38
+ } finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
42
+
43
+ export async function fetchTemplates(query?: string): Promise<HubTemplate[]> {
44
+ const qs = query ? `?q=${encodeURIComponent(query)}` : '';
45
+ return hubFetch<HubTemplate[]>(`/api/templates${qs}`);
46
+ }
47
+
48
+ export async function fetchTemplate(id: string): Promise<HubTemplateDetail> {
49
+ return hubFetch<HubTemplateDetail>(`/api/templates/${encodeURIComponent(id)}`);
50
+ }
51
+
52
+ export async function fetchBrainSeeds(templateId: string): Promise<HubBrainSeed[]> {
53
+ return hubFetch<HubBrainSeed[]>(`/api/templates/${encodeURIComponent(templateId)}/brain-seeds`);
54
+ }
55
+
56
+ export function isHubAvailable(): Promise<boolean> {
57
+ return hubFetch<{ ok: boolean }>('/api/health')
58
+ .then(() => true)
59
+ .catch(() => false);
60
+ }
package/src/index.ts CHANGED
@@ -176,7 +176,9 @@ export { Scheduler, parseCron, cronMatches } from './core/scheduler';
176
176
  export type { CronJob, JobHandler } from './core/scheduler';
177
177
 
178
178
  // v1.5.0 — built-in tools + MCP client
179
- export { getBuiltinTools, getBuiltinToolsByName, rlTools, homeAssistantTools, configureHomeAssistant } from './tools/builtin';
179
+ export { getBuiltinTools, getBuiltinToolsByName, rlTools, homeAssistantTools, configureHomeAssistant, webSearchTools, webSearchTool, webReadTool } from './tools/builtin';
180
+ export { webSearch, parseDuckDuckGoHTML, type SearchResult as WebSearchResult, type WebSearchConfig, type SearchEngine } from './tools/web-search';
181
+ export { scrapeUrl, extractReadableContent, type ScrapedContent } from './tools/web-scraper';
180
182
  export { MCPClient } from './tools/mcp-client';
181
183
  export type { MCPServerConfig } from './tools/mcp-client';
182
184
 
@@ -255,7 +257,7 @@ export type { SMSChannelConfig } from './channels/sms';
255
257
  export { VoiceCallManager } from './channels/voice-call';
256
258
  export type { VoiceCallConfig } from './channels/voice-call';
257
259
  export { IDEBridge } from './core/ide-bridge';
258
- export type { IDEConfig, Diagnostic, TextEdit, Range, SearchOptions, SearchResult } from './core/ide-bridge';
260
+ export type { IDEConfig, Diagnostic, TextEdit, Range, SearchOptions, SearchResult as IDESearchResult } from './core/ide-bridge';
259
261
  export { NodeNetwork } from './core/node-network';
260
262
  export type { RemoteNode } from './core/node-network';
261
263
  export { Gateway } from './core/gateway';
@@ -325,9 +325,72 @@ function isGeminiNative(): boolean {
325
325
  return key.startsWith('AQ.') || (baseUrl.includes('googleapis.com') && !baseUrl.includes('/openai'));
326
326
  }
327
327
 
328
+ class ClaudeCLIProvider implements LLMProvider {
329
+ name = 'claude-cli';
330
+ private model: string;
331
+
332
+ constructor(model?: string) {
333
+ this.model = model || 'sonnet';
334
+ }
335
+
336
+ async chat(messages: Message[], systemPrompt?: string, options?: ChatOptions): Promise<string> {
337
+ const { execFile } = await import('child_process');
338
+ const { promisify } = await import('util');
339
+ const execFileAsync = promisify(execFile);
340
+
341
+ // Build the prompt from messages
342
+ const lastMessage = messages[messages.length - 1];
343
+ if (!lastMessage) return '';
344
+
345
+ let prompt = lastMessage.content;
346
+
347
+ // Add tool prompt if tools provided
348
+ if (options?.tools && options.tools.length > 0) {
349
+ prompt += buildToolPrompt(options.tools);
350
+ }
351
+
352
+ const args = ['--print'];
353
+ if (systemPrompt) {
354
+ args.push('--system-prompt', systemPrompt);
355
+ }
356
+ if (this.model) {
357
+ args.push('--model', this.model);
358
+ }
359
+ args.push(prompt);
360
+
361
+ try {
362
+ const { stdout } = await execFileAsync('claude', args, {
363
+ timeout: 120_000,
364
+ maxBuffer: 10 * 1024 * 1024,
365
+ env: { ...process.env },
366
+ });
367
+ return stdout.trim();
368
+ } catch (err: any) {
369
+ if (err.code === 'ENOENT') {
370
+ throw new Error(
371
+ 'Claude CLI not found. Install it: npm install -g @anthropic-ai/claude-code\n' +
372
+ 'Then authenticate: claude login'
373
+ );
374
+ }
375
+ throw new Error(`Claude CLI error: ${err.message}`);
376
+ }
377
+ }
378
+
379
+ async *chatStream(messages: Message[], systemPrompt?: string): AsyncIterable<string> {
380
+ // Claude CLI --print doesn't support streaming well, so we do single-shot
381
+ const result = await this.chat(messages, systemPrompt);
382
+ yield result;
383
+ }
384
+ }
385
+
328
386
  export function createProvider(name: string = 'openai', model?: string, baseUrl?: string, apiKey?: string): LLMProvider {
329
387
  const finalModel = model || process.env.OPC_LLM_MODEL || 'gpt-4o-mini';
330
388
 
389
+ // Claude CLI mode: use local claude command (Claude Max/Pro subscription)
390
+ if (name === 'claude-cli' || process.env.OPC_LLM_PROVIDER === 'claude-cli') {
391
+ return new ClaudeCLIProvider(finalModel !== 'gpt-4o-mini' ? finalModel : undefined);
392
+ }
393
+
331
394
  if (name === 'ollama') {
332
395
  const ollamaBase = baseUrl || process.env.OPC_LLM_BASE_URL || 'http://localhost:11434/v1';
333
396
  const ollamaKey = apiKey || process.env.OPC_LLM_API_KEY || 'ollama';
@@ -351,4 +414,4 @@ export function createProvider(name: string = 'openai', model?: string, baseUrl?
351
414
  return new OpenAICompatibleProvider(resolvedName, finalModel, baseUrl, apiKey);
352
415
  }
353
416
 
354
- export const SUPPORTED_PROVIDERS = ['openai', 'ollama', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'] as const;
417
+ export const SUPPORTED_PROVIDERS = ['openai', 'ollama', 'claude-cli', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'] as const;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Cron Engine — persistent scheduler with file-based storage.
3
+ * Manages scheduled tasks with cron expressions, persists to ~/.opc/schedules.json,
4
+ * and auto-recovers on startup.
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import * as os from 'os';
10
+ import { parseCron, cronMatches, Scheduler } from '../core/scheduler';
11
+ import type { CronJob, JobHandler } from '../core/scheduler';
12
+
13
+ export interface ScheduleTask {
14
+ id: string;
15
+ name: string;
16
+ schedule: string; // cron expression
17
+ description: string; // natural language description
18
+ frequency: 'daily' | 'weekly' | 'monthly' | 'custom';
19
+ time?: string; // HH:mm for simple schedules
20
+ outputChannel: 'telegram' | 'email' | 'web';
21
+ enabled: boolean;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ lastRun?: string;
25
+ nextRun?: string;
26
+ }
27
+
28
+ export interface SchedulesStore {
29
+ tasks: ScheduleTask[];
30
+ }
31
+
32
+ function getSchedulesPath(): string {
33
+ const dir = join(os.homedir(), '.opc');
34
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
35
+ return join(dir, 'schedules.json');
36
+ }
37
+
38
+ function loadSchedules(): SchedulesStore {
39
+ const p = getSchedulesPath();
40
+ if (existsSync(p)) {
41
+ try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { /* ignore */ }
42
+ }
43
+ return { tasks: [] };
44
+ }
45
+
46
+ function saveSchedules(store: SchedulesStore): void {
47
+ writeFileSync(getSchedulesPath(), JSON.stringify(store, null, 2));
48
+ }
49
+
50
+ /** Convert frequency + time to cron expression */
51
+ export function frequencyToCron(frequency: string, time?: string): string {
52
+ const [hour, minute] = (time || '09:00').split(':').map(Number);
53
+ switch (frequency) {
54
+ case 'daily': return `${minute} ${hour} * * *`;
55
+ case 'weekly': return `${minute} ${hour} * * 1`;
56
+ case 'monthly': return `${minute} ${hour} 1 * *`;
57
+ default: return '0 9 * * *'; // fallback
58
+ }
59
+ }
60
+
61
+ /** Compute next run from a cron expression */
62
+ function computeNextRun(cronExpr: string): string | undefined {
63
+ try {
64
+ const parsed = parseCron(cronExpr);
65
+ const d = new Date();
66
+ d.setSeconds(0, 0);
67
+ d.setMinutes(d.getMinutes() + 1);
68
+ for (let i = 0; i < 48 * 60; i++) {
69
+ if (cronMatches(parsed, d)) return d.toISOString();
70
+ d.setMinutes(d.getMinutes() + 1);
71
+ }
72
+ } catch { /* ignore */ }
73
+ return undefined;
74
+ }
75
+
76
+ export class CronEngine {
77
+ private scheduler: Scheduler;
78
+ private store: SchedulesStore;
79
+ private handler: JobHandler;
80
+
81
+ constructor(handler?: JobHandler) {
82
+ this.handler = handler || (async (job) => {
83
+ console.log(`[cron-engine] Executing job: ${job.name} (${job.id})`);
84
+ });
85
+ this.scheduler = new Scheduler(this.handler);
86
+ this.store = loadSchedules();
87
+ }
88
+
89
+ /** Initialize and recover persisted tasks */
90
+ start(): void {
91
+ for (const task of this.store.tasks) {
92
+ if (task.enabled) {
93
+ this.scheduler.addJob({
94
+ id: task.id,
95
+ name: task.name,
96
+ schedule: task.schedule,
97
+ task: task.description,
98
+ enabled: true,
99
+ });
100
+ }
101
+ }
102
+ this.scheduler.start();
103
+ console.log(`[cron-engine] Started with ${this.store.tasks.filter(t => t.enabled).length} active tasks`);
104
+ }
105
+
106
+ stop(): void {
107
+ this.scheduler.stop();
108
+ }
109
+
110
+ listTasks(): ScheduleTask[] {
111
+ // Refresh nextRun
112
+ return this.store.tasks.map(t => ({
113
+ ...t,
114
+ nextRun: t.enabled ? computeNextRun(t.schedule) : undefined,
115
+ }));
116
+ }
117
+
118
+ getTask(id: string): ScheduleTask | undefined {
119
+ return this.store.tasks.find(t => t.id === id);
120
+ }
121
+
122
+ createTask(input: Omit<ScheduleTask, 'id' | 'createdAt' | 'updatedAt' | 'lastRun' | 'nextRun'>): ScheduleTask {
123
+ const id = `sched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
124
+ const now = new Date().toISOString();
125
+ const schedule = input.schedule || frequencyToCron(input.frequency, input.time);
126
+ const task: ScheduleTask = {
127
+ ...input,
128
+ id,
129
+ schedule,
130
+ createdAt: now,
131
+ updatedAt: now,
132
+ nextRun: input.enabled ? computeNextRun(schedule) : undefined,
133
+ };
134
+ this.store.tasks.push(task);
135
+ saveSchedules(this.store);
136
+
137
+ if (task.enabled) {
138
+ this.scheduler.addJob({
139
+ id: task.id,
140
+ name: task.name,
141
+ schedule: task.schedule,
142
+ task: task.description,
143
+ enabled: true,
144
+ });
145
+ }
146
+ return task;
147
+ }
148
+
149
+ updateTask(id: string, updates: Partial<ScheduleTask>): ScheduleTask | null {
150
+ const idx = this.store.tasks.findIndex(t => t.id === id);
151
+ if (idx === -1) return null;
152
+
153
+ const task = { ...this.store.tasks[idx], ...updates, id, updatedAt: new Date().toISOString() };
154
+ if (updates.frequency || updates.time) {
155
+ task.schedule = updates.schedule || frequencyToCron(task.frequency, task.time);
156
+ }
157
+ task.nextRun = task.enabled ? computeNextRun(task.schedule) : undefined;
158
+ this.store.tasks[idx] = task;
159
+ saveSchedules(this.store);
160
+
161
+ // Update scheduler
162
+ this.scheduler.removeJob(id);
163
+ if (task.enabled) {
164
+ this.scheduler.addJob({
165
+ id: task.id,
166
+ name: task.name,
167
+ schedule: task.schedule,
168
+ task: task.description,
169
+ enabled: true,
170
+ });
171
+ }
172
+ return task;
173
+ }
174
+
175
+ deleteTask(id: string): boolean {
176
+ const idx = this.store.tasks.findIndex(t => t.id === id);
177
+ if (idx === -1) return false;
178
+ this.store.tasks.splice(idx, 1);
179
+ saveSchedules(this.store);
180
+ this.scheduler.removeJob(id);
181
+ return true;
182
+ }
183
+
184
+ async runTask(id: string): Promise<boolean> {
185
+ const task = this.store.tasks.find(t => t.id === id);
186
+ if (!task) return false;
187
+ task.lastRun = new Date().toISOString();
188
+ saveSchedules(this.store);
189
+ return this.scheduler.runJob(id);
190
+ }
191
+ }
@@ -0,0 +1,2 @@
1
+ export { CronEngine, frequencyToCron } from './cron-engine';
2
+ export type { ScheduleTask, SchedulesStore } from './cron-engine';