@yeaft/webchat-agent 0.1.442 → 0.1.443

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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * image-generation.js — Generate images via external API.
3
+ *
4
+ * Delegates to a configured image generation service (DALL-E, etc.).
5
+ */
6
+
7
+ import { defineTool } from './types.js';
8
+
9
+ export default defineTool({
10
+ name: 'ImageGeneration',
11
+ description: `Generate an image from a text description.
12
+
13
+ Uses a configured image generation API to create images.
14
+ Requires an image generation API endpoint in config.
15
+
16
+ Guidelines:
17
+ - Provide detailed, specific descriptions for best results
18
+ - Specify style, composition, and mood
19
+ - Images are saved to the working directory`,
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ prompt: {
24
+ type: 'string',
25
+ description: 'Text description of the image to generate',
26
+ },
27
+ output_path: {
28
+ type: 'string',
29
+ description: 'File path to save the generated image',
30
+ },
31
+ size: {
32
+ type: 'string',
33
+ enum: ['256x256', '512x512', '1024x1024'],
34
+ description: 'Image size (default: "1024x1024")',
35
+ },
36
+ },
37
+ required: ['prompt'],
38
+ },
39
+ modes: ['chat', 'work'],
40
+ isConcurrencySafe: () => true,
41
+ isReadOnly: () => false,
42
+ async execute(input, ctx) {
43
+ const { prompt, output_path, size = '1024x1024' } = input;
44
+ if (!prompt) return JSON.stringify({ error: 'prompt is required' });
45
+
46
+ const imageApiUrl = ctx?.config?.imageApiUrl;
47
+ if (!imageApiUrl) {
48
+ return JSON.stringify({
49
+ error: 'No image generation API configured.',
50
+ hint: 'Configure imageApiUrl in ~/.yeaft/config.json',
51
+ });
52
+ }
53
+
54
+ try {
55
+ const response = await fetch(imageApiUrl, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ prompt, size }),
59
+ signal: ctx?.signal,
60
+ });
61
+
62
+ if (!response.ok) {
63
+ return JSON.stringify({ error: `Image API returned ${response.status}: ${response.statusText}` });
64
+ }
65
+
66
+ const data = await response.json();
67
+
68
+ // If output_path specified, save the image
69
+ if (output_path && data.url) {
70
+ const { resolve: resolvePath } = await import('path');
71
+ const { writeFile } = await import('fs/promises');
72
+
73
+ const imgResponse = await fetch(data.url);
74
+ const buffer = Buffer.from(await imgResponse.arrayBuffer());
75
+ const absPath = resolvePath(ctx?.cwd || process.cwd(), output_path);
76
+ await writeFile(absPath, buffer);
77
+
78
+ return JSON.stringify({
79
+ success: true,
80
+ path: absPath,
81
+ size,
82
+ prompt: prompt.slice(0, 100),
83
+ });
84
+ }
85
+
86
+ return JSON.stringify({
87
+ success: true,
88
+ url: data.url,
89
+ size,
90
+ prompt: prompt.slice(0, 100),
91
+ });
92
+ } catch (err) {
93
+ if (err.name === 'AbortError') return JSON.stringify({ error: 'Generation cancelled' });
94
+ return JSON.stringify({ error: `Image generation failed: ${err.message}` });
95
+ }
96
+ },
97
+ });
@@ -9,21 +9,113 @@
9
9
  */
10
10
 
11
11
  import { ToolRegistry } from './registry.js';
12
+
13
+ // --- Existing tools ---
12
14
  import mcpTools from './mcp-tools.js';
13
15
  import skillTool from './skill.js';
14
16
  import enterWorktree from './enter-worktree.js';
15
17
  import exitWorktree from './exit-worktree.js';
16
18
 
19
+ // --- P0 Core tools ---
20
+ import askUser from './ask-user.js';
21
+ import memoryRead from './memory-read.js';
22
+ import memoryWrite from './memory-write.js';
23
+ import memorySearch from './memory-search.js';
24
+ import webSearch from './web-search.js';
25
+ import webFetch from './web-fetch.js';
26
+ import historySearch from './history-search.js';
27
+
28
+ // --- P0 File tools ---
29
+ import bash from './bash.js';
30
+ import fileRead from './file-read.js';
31
+ import fileWrite from './file-write.js';
32
+ import fileEdit from './file-edit.js';
33
+ import globTool from './glob.js';
34
+ import grepTool from './grep.js';
35
+ import listDir from './list-dir.js';
36
+ import applyPatch from './apply-patch.js';
37
+
38
+ // --- P1 Agent tools ---
39
+ import agentTool from './agent.js';
40
+ import sendMessage from './send-message.js';
41
+ import waitAgent from './wait-agent.js';
42
+ import closeAgent from './close-agent.js';
43
+ import listAgents from './list-agents.js';
44
+
45
+ // --- P1 Task tools ---
46
+ import {
47
+ taskCreate,
48
+ taskUpdate,
49
+ taskList,
50
+ taskGet,
51
+ followupTask,
52
+ updatePlan,
53
+ } from './task-tools.js';
54
+
55
+ // --- P2 Auxiliary tools ---
56
+ import { jsRepl, jsReplReset } from './js-repl.js';
57
+ import notebookEdit from './notebook-edit.js';
58
+ import imageGeneration from './image-generation.js';
59
+ import viewImage from './view-image.js';
60
+ import toolSearch from './tool-search.js';
61
+ import requestPermissions from './request-permissions.js';
62
+ import writeStdin from './write-stdin.js';
63
+
17
64
  /**
18
65
  * All built-in tools, flattened into a single array.
19
66
  * mcpTools is already an array; the rest are single ToolDef objects.
20
67
  * @type {import('./types.js').ToolDef[]}
21
68
  */
22
69
  export const allTools = [
70
+ // Existing tools
23
71
  ...mcpTools,
24
72
  skillTool,
25
73
  enterWorktree,
26
74
  exitWorktree,
75
+
76
+ // P0 Core
77
+ askUser,
78
+ memoryRead,
79
+ memoryWrite,
80
+ memorySearch,
81
+ webSearch,
82
+ webFetch,
83
+ historySearch,
84
+
85
+ // P0 File
86
+ bash,
87
+ fileRead,
88
+ fileWrite,
89
+ fileEdit,
90
+ globTool,
91
+ grepTool,
92
+ listDir,
93
+ applyPatch,
94
+
95
+ // P1 Agent
96
+ agentTool,
97
+ sendMessage,
98
+ waitAgent,
99
+ closeAgent,
100
+ listAgents,
101
+
102
+ // P1 Task
103
+ taskCreate,
104
+ taskUpdate,
105
+ taskList,
106
+ taskGet,
107
+ followupTask,
108
+ updatePlan,
109
+
110
+ // P2 Auxiliary
111
+ jsRepl,
112
+ jsReplReset,
113
+ notebookEdit,
114
+ imageGeneration,
115
+ viewImage,
116
+ toolSearch,
117
+ requestPermissions,
118
+ writeStdin,
27
119
  ];
28
120
 
29
121
  /**
@@ -0,0 +1,122 @@
1
+ /**
2
+ * js-repl.js — JavaScript REPL for evaluating expressions.
3
+ *
4
+ * Runs JavaScript code in a persistent VM context, allowing
5
+ * state to be maintained across calls.
6
+ */
7
+
8
+ import { defineTool } from './types.js';
9
+ import { createContext, runInContext } from 'vm';
10
+
11
+ /** Persistent VM context per session. */
12
+ let vmContext = null;
13
+
14
+ function getContext() {
15
+ if (!vmContext) {
16
+ vmContext = createContext({
17
+ console: {
18
+ log: (...args) => { vmContext.__output.push(args.map(String).join(' ')); },
19
+ error: (...args) => { vmContext.__output.push('[error] ' + args.map(String).join(' ')); },
20
+ warn: (...args) => { vmContext.__output.push('[warn] ' + args.map(String).join(' ')); },
21
+ },
22
+ setTimeout,
23
+ setInterval,
24
+ clearTimeout,
25
+ clearInterval,
26
+ JSON,
27
+ Math,
28
+ Date,
29
+ RegExp,
30
+ Array,
31
+ Object,
32
+ String,
33
+ Number,
34
+ Boolean,
35
+ Map,
36
+ Set,
37
+ WeakMap,
38
+ WeakSet,
39
+ Promise,
40
+ Error,
41
+ Buffer,
42
+ __output: [],
43
+ });
44
+ }
45
+ vmContext.__output = [];
46
+ return vmContext;
47
+ }
48
+
49
+ export const jsRepl = defineTool({
50
+ name: 'JsRepl',
51
+ description: `Evaluate JavaScript code in a persistent REPL environment.
52
+
53
+ The REPL context persists across calls — variables and functions
54
+ defined in one call are available in subsequent calls.
55
+
56
+ Guidelines:
57
+ - Use for calculations, data transformations, and quick experiments
58
+ - State is preserved between calls (use JsReplReset to clear)
59
+ - console.log output is captured and returned
60
+ - Returns the last expression's value plus any console output
61
+ - No filesystem or network access from within the REPL`,
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ code: {
66
+ type: 'string',
67
+ description: 'JavaScript code to evaluate',
68
+ },
69
+ },
70
+ required: ['code'],
71
+ },
72
+ modes: ['chat', 'work'],
73
+ isConcurrencySafe: () => false,
74
+ isReadOnly: () => true,
75
+ async execute(input, ctx) {
76
+ const { code } = input;
77
+ if (!code) return JSON.stringify({ error: 'code is required' });
78
+
79
+ const vmCtx = getContext();
80
+
81
+ try {
82
+ const result = runInContext(code, vmCtx, {
83
+ timeout: 10000, // 10 second timeout
84
+ displayErrors: true,
85
+ });
86
+
87
+ const output = vmCtx.__output.slice();
88
+ const resultStr = result === undefined ? '' : String(result);
89
+
90
+ const parts = [];
91
+ if (output.length > 0) parts.push(output.join('\n'));
92
+ if (resultStr) parts.push(`→ ${resultStr}`);
93
+
94
+ return parts.join('\n') || '(no output)';
95
+ } catch (err) {
96
+ const output = vmCtx.__output.slice();
97
+ const parts = [];
98
+ if (output.length > 0) parts.push(output.join('\n'));
99
+ parts.push(`Error: ${err.message}`);
100
+ return parts.join('\n');
101
+ }
102
+ },
103
+ });
104
+
105
+ export const jsReplReset = defineTool({
106
+ name: 'JsReplReset',
107
+ description: `Reset the JavaScript REPL environment.
108
+
109
+ Clears all variables and state from previous evaluations.
110
+ Use when you want a clean slate.`,
111
+ parameters: {
112
+ type: 'object',
113
+ properties: {},
114
+ },
115
+ modes: ['chat', 'work'],
116
+ isConcurrencySafe: () => false,
117
+ isReadOnly: () => false,
118
+ async execute(input, ctx) {
119
+ vmContext = null;
120
+ return JSON.stringify({ success: true, message: 'REPL context reset' });
121
+ },
122
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * list-agents.js — List all active sub-agents.
3
+ */
4
+
5
+ import { defineTool } from './types.js';
6
+ import { getAgentRegistry } from './agent.js';
7
+
8
+ export default defineTool({
9
+ name: 'ListAgents',
10
+ description: `List all sub-agents and their current status.
11
+
12
+ Shows agent IDs, names, tasks, status (created/active/completed/closed),
13
+ and message counts. Use to monitor parallel task progress.`,
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ include_closed: {
18
+ type: 'boolean',
19
+ description: 'Include closed agents in the list (default: false)',
20
+ },
21
+ },
22
+ },
23
+ modes: ['work'],
24
+ isConcurrencySafe: () => true,
25
+ isReadOnly: () => true,
26
+ async execute(input, ctx) {
27
+ const { include_closed = false } = input;
28
+ const agents = getAgentRegistry();
29
+
30
+ const agentList = [];
31
+ for (const [id, agent] of agents) {
32
+ if (!include_closed && agent.status === 'closed') continue;
33
+ agentList.push({
34
+ id,
35
+ name: agent.name,
36
+ status: agent.status,
37
+ task: agent.task?.slice(0, 200),
38
+ messages: agent.messages.length,
39
+ hasResult: !!agent.result,
40
+ createdAt: agent.createdAt,
41
+ });
42
+ }
43
+
44
+ if (agentList.length === 0) {
45
+ return JSON.stringify({
46
+ agents: [],
47
+ message: 'No active sub-agents',
48
+ });
49
+ }
50
+
51
+ return JSON.stringify({
52
+ agents: agentList,
53
+ totalCount: agentList.length,
54
+ }, null, 2);
55
+ },
56
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * list-dir.js — List directory contents.
3
+ *
4
+ * Lists files and directories with type, size, and modification time.
5
+ * Skips common large directories (node_modules, .git, etc.).
6
+ */
7
+
8
+ import { defineTool } from './types.js';
9
+ import { readdir, stat } from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import { resolve, join } from 'path';
12
+
13
+ /** Directories to skip in listings. */
14
+ const SKIP_DIRS = new Set([
15
+ 'node_modules', '.git', '__pycache__', '.next', '.nuxt', '.cache',
16
+ ]);
17
+
18
+ export default defineTool({
19
+ name: 'ListDir',
20
+ description: `List the contents of a directory.
21
+
22
+ Shows files and subdirectories with their types and sizes.
23
+ Directories are listed first, then files, both sorted alphabetically.
24
+ Common large directories (node_modules, .git) are skipped.
25
+
26
+ This is better than using Bash with 'ls' because it provides structured output.`,
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ path: {
31
+ type: 'string',
32
+ description: 'Directory path to list (default: cwd)',
33
+ },
34
+ show_hidden: {
35
+ type: 'boolean',
36
+ description: 'Include hidden files (starting with dot, default: true)',
37
+ },
38
+ },
39
+ },
40
+ modes: ['work'],
41
+ isConcurrencySafe: () => true,
42
+ isReadOnly: () => true,
43
+ async execute(input, ctx) {
44
+ const { path: dirPath, show_hidden = true } = input;
45
+
46
+ const cwd = ctx?.cwd || process.cwd();
47
+ const absPath = dirPath ? resolve(cwd, dirPath) : cwd;
48
+
49
+ if (!existsSync(absPath)) {
50
+ return JSON.stringify({ error: `Directory not found: ${absPath}` });
51
+ }
52
+
53
+ try {
54
+ const entries = await readdir(absPath, { withFileTypes: true });
55
+ const results = [];
56
+
57
+ for (const entry of entries) {
58
+ // Skip hidden files if not requested
59
+ if (!show_hidden && entry.name.startsWith('.')) continue;
60
+
61
+ // Skip large directories
62
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
63
+
64
+ try {
65
+ const fullPath = join(absPath, entry.name);
66
+ const fileStat = await stat(fullPath);
67
+ results.push({
68
+ name: entry.name,
69
+ type: entry.isDirectory() ? 'dir' : 'file',
70
+ size: fileStat.size,
71
+ modified: fileStat.mtime.toISOString(),
72
+ });
73
+ } catch {
74
+ results.push({
75
+ name: entry.name,
76
+ type: entry.isDirectory() ? 'dir' : 'file',
77
+ size: 0,
78
+ });
79
+ }
80
+ }
81
+
82
+ // Sort: directories first, then files, alphabetically
83
+ results.sort((a, b) => {
84
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
85
+ return a.name.localeCompare(b.name);
86
+ });
87
+
88
+ // Format as text
89
+ const lines = results.map(r => {
90
+ const typeChar = r.type === 'dir' ? '📁' : '📄';
91
+ const sizeStr = r.type === 'dir' ? '' : ` (${formatSize(r.size)})`;
92
+ return `${typeChar} ${r.name}${sizeStr}`;
93
+ });
94
+
95
+ return `${absPath}/\n\n${lines.join('\n')}` || `${absPath}/ (empty directory)`;
96
+ } catch (err) {
97
+ return JSON.stringify({ error: `Failed to list directory: ${err.message}` });
98
+ }
99
+ },
100
+ });
101
+
102
+ function formatSize(bytes) {
103
+ if (bytes < 1024) return `${bytes}B`;
104
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
105
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
106
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * memory-read.js — Read memory entries from the Yeaft memory store.
3
+ *
4
+ * Reads the user profile (MEMORY.md), specific sections, or individual
5
+ * memory entries by name.
6
+ */
7
+
8
+ import { defineTool } from './types.js';
9
+
10
+ export default defineTool({
11
+ name: 'MemoryRead',
12
+ description: `Read from Yeaft's persistent memory system.
13
+
14
+ Actions:
15
+ - "profile" — read the full MEMORY.md user profile
16
+ - "section" — read a specific section from MEMORY.md (e.g. "Facts", "Preferences")
17
+ - "entry" — read a specific memory entry by name
18
+ - "list" — list all memory entries (frontmatter only, no body)
19
+ - "scopes" — list all memory scopes and their entry counts`,
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ action: {
24
+ type: 'string',
25
+ enum: ['profile', 'section', 'entry', 'list', 'scopes'],
26
+ description: 'What to read from memory',
27
+ },
28
+ name: {
29
+ type: 'string',
30
+ description: 'Entry name slug (for "entry" action) or section name (for "section" action)',
31
+ },
32
+ },
33
+ required: ['action'],
34
+ },
35
+ modes: ['chat', 'work'],
36
+ isConcurrencySafe: () => true,
37
+ isReadOnly: () => true,
38
+ async execute(input, ctx) {
39
+ const memoryStore = ctx?.memoryStore;
40
+ if (!memoryStore) {
41
+ return JSON.stringify({ error: 'Memory system not initialized' });
42
+ }
43
+
44
+ try {
45
+ switch (input.action) {
46
+ case 'profile': {
47
+ const profile = memoryStore.readProfile();
48
+ return profile || '(No profile found — MEMORY.md is empty)';
49
+ }
50
+
51
+ case 'section': {
52
+ if (!input.name) return JSON.stringify({ error: 'name is required for "section" action' });
53
+ const section = memoryStore.readSection(input.name);
54
+ return section || `(Section "${input.name}" not found in MEMORY.md)`;
55
+ }
56
+
57
+ case 'entry': {
58
+ if (!input.name) return JSON.stringify({ error: 'name is required for "entry" action' });
59
+ const entry = memoryStore.readEntry(input.name);
60
+ if (!entry) return JSON.stringify({ error: `Entry "${input.name}" not found` });
61
+ return JSON.stringify(entry, null, 2);
62
+ }
63
+
64
+ case 'list': {
65
+ const entries = memoryStore.listEntries();
66
+ return JSON.stringify({
67
+ entries: entries.map(e => ({
68
+ name: e.name,
69
+ kind: e.kind,
70
+ scope: e.scope,
71
+ tags: e.tags,
72
+ importance: e.importance,
73
+ updated_at: e.updated_at,
74
+ })),
75
+ totalCount: entries.length,
76
+ }, null, 2);
77
+ }
78
+
79
+ case 'scopes': {
80
+ const scopes = memoryStore.readScopes();
81
+ return JSON.stringify({ scopes }, null, 2);
82
+ }
83
+
84
+ default:
85
+ return JSON.stringify({ error: `Unknown action: ${input.action}` });
86
+ }
87
+ } catch (err) {
88
+ return JSON.stringify({ error: `Memory read failed: ${err.message}` });
89
+ }
90
+ },
91
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * memory-search.js — Search memory entries by scope, tags, and keywords.
3
+ *
4
+ * Uses the MemoryStore's findByFilter for structured search.
5
+ */
6
+
7
+ import { defineTool } from './types.js';
8
+
9
+ export default defineTool({
10
+ name: 'MemorySearch',
11
+ description: `Search Yeaft's persistent memory for relevant entries.
12
+
13
+ Searches by scope, tags, kind, or keyword. Results are scored by relevance:
14
+ - Exact scope match: highest score
15
+ - Ancestor/descendant scope: medium score
16
+ - Tag overlap: additional score per matching tag
17
+ - Keyword in content: found via full-text scan
18
+
19
+ Use this to find previously learned information before asking the user again.`,
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ scope: {
24
+ type: 'string',
25
+ description: 'Memory scope to search in (e.g. "global", "work/my-project")',
26
+ },
27
+ tags: {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ description: 'Tags to filter by',
31
+ },
32
+ kind: {
33
+ type: 'string',
34
+ enum: ['fact', 'preference', 'skill', 'lesson', 'context', 'relation'],
35
+ description: 'Filter by memory kind',
36
+ },
37
+ keyword: {
38
+ type: 'string',
39
+ description: 'Keyword to search in entry content',
40
+ },
41
+ limit: {
42
+ type: 'number',
43
+ description: 'Maximum number of results (default: 15)',
44
+ },
45
+ },
46
+ },
47
+ modes: ['chat', 'work'],
48
+ isConcurrencySafe: () => true,
49
+ isReadOnly: () => true,
50
+ async execute(input, ctx) {
51
+ const memoryStore = ctx?.memoryStore;
52
+ if (!memoryStore) {
53
+ return JSON.stringify({ error: 'Memory system not initialized' });
54
+ }
55
+
56
+ try {
57
+ const limit = input.limit || 15;
58
+
59
+ // Use findByFilter for scope + tag search
60
+ let results = memoryStore.findByFilter({
61
+ scope: input.scope,
62
+ tags: input.tags || [],
63
+ limit: limit * 2, // over-fetch for post-filtering
64
+ });
65
+
66
+ // Filter by kind if specified
67
+ if (input.kind) {
68
+ results = results.filter(e => e.kind === input.kind);
69
+ }
70
+
71
+ // Filter by keyword if specified
72
+ if (input.keyword) {
73
+ const kw = input.keyword.toLowerCase();
74
+ results = results.filter(e =>
75
+ (e.content && e.content.toLowerCase().includes(kw)) ||
76
+ (e.name && e.name.toLowerCase().includes(kw)) ||
77
+ (e.tags && e.tags.some(t => t.toLowerCase().includes(kw)))
78
+ );
79
+ }
80
+
81
+ // Trim to limit
82
+ results = results.slice(0, limit);
83
+
84
+ return JSON.stringify({
85
+ results: results.map(e => ({
86
+ name: e.name,
87
+ kind: e.kind,
88
+ scope: e.scope,
89
+ tags: e.tags,
90
+ importance: e.importance,
91
+ content: e.content?.slice(0, 500) + (e.content?.length > 500 ? '...' : ''),
92
+ updated_at: e.updated_at,
93
+ score: e._score,
94
+ })),
95
+ totalResults: results.length,
96
+ }, null, 2);
97
+ } catch (err) {
98
+ return JSON.stringify({ error: `Memory search failed: ${err.message}` });
99
+ }
100
+ },
101
+ });