@yeaft/webchat-agent 0.1.442 → 0.1.444
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/connection/message-router.js +5 -1
- package/package.json +1 -1
- package/unify/config.js +18 -0
- package/unify/tools/agent.js +89 -0
- package/unify/tools/apply-patch.js +176 -0
- package/unify/tools/ask-user.js +62 -0
- package/unify/tools/bash.js +189 -0
- package/unify/tools/close-agent.js +58 -0
- package/unify/tools/file-edit.js +120 -0
- package/unify/tools/file-read.js +125 -0
- package/unify/tools/file-write.js +73 -0
- package/unify/tools/glob.js +143 -0
- package/unify/tools/grep.js +268 -0
- package/unify/tools/history-search.js +69 -0
- package/unify/tools/image-generation.js +97 -0
- package/unify/tools/index.js +92 -0
- package/unify/tools/js-repl.js +122 -0
- package/unify/tools/list-agents.js +56 -0
- package/unify/tools/list-dir.js +106 -0
- package/unify/tools/memory-read.js +91 -0
- package/unify/tools/memory-search.js +101 -0
- package/unify/tools/memory-write.js +114 -0
- package/unify/tools/notebook-edit.js +132 -0
- package/unify/tools/request-permissions.js +60 -0
- package/unify/tools/send-message.js +62 -0
- package/unify/tools/task-tools.js +358 -0
- package/unify/tools/tool-search.js +97 -0
- package/unify/tools/view-image.js +117 -0
- package/unify/tools/wait-agent.js +84 -0
- package/unify/tools/web-fetch.js +131 -0
- package/unify/tools/web-search.js +80 -0
- package/unify/tools/write-stdin.js +54 -0
- package/unify/web-bridge.js +27 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-write.js — Write memory entries to the Yeaft memory store.
|
|
3
|
+
*
|
|
4
|
+
* Creates, updates, or deletes memory entries. Also supports
|
|
5
|
+
* appending lines to MEMORY.md sections and overwriting the profile.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
|
|
10
|
+
export default defineTool({
|
|
11
|
+
name: 'MemoryWrite',
|
|
12
|
+
description: `Write to Yeaft's persistent memory system.
|
|
13
|
+
|
|
14
|
+
Actions:
|
|
15
|
+
- "write_entry" — create or update a memory entry (entries/*.md)
|
|
16
|
+
- "delete_entry" — delete a memory entry by name
|
|
17
|
+
- "write_profile" — overwrite the full MEMORY.md profile
|
|
18
|
+
- "add_to_section" — append a line to a section in MEMORY.md
|
|
19
|
+
|
|
20
|
+
Memory kinds: fact, preference, skill, lesson, context, relation
|
|
21
|
+
Importance levels: low, normal, high, critical`,
|
|
22
|
+
parameters: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
action: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
enum: ['write_entry', 'delete_entry', 'write_profile', 'add_to_section'],
|
|
28
|
+
description: 'What memory operation to perform',
|
|
29
|
+
},
|
|
30
|
+
entry: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
description: 'Memory entry data (for "write_entry")',
|
|
33
|
+
properties: {
|
|
34
|
+
name: { type: 'string', description: 'Entry name (will be slugified for filename)' },
|
|
35
|
+
kind: { type: 'string', enum: ['fact', 'preference', 'skill', 'lesson', 'context', 'relation'] },
|
|
36
|
+
scope: { type: 'string', description: 'Scope path, e.g. "global", "work/my-project"' },
|
|
37
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
38
|
+
importance: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
|
|
39
|
+
content: { type: 'string', description: 'The memory content (markdown body)' },
|
|
40
|
+
},
|
|
41
|
+
required: ['name', 'content'],
|
|
42
|
+
},
|
|
43
|
+
name: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Entry name slug (for "delete_entry") or section name (for "add_to_section")',
|
|
46
|
+
},
|
|
47
|
+
content: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Content for "write_profile" or line to add for "add_to_section"',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['action'],
|
|
53
|
+
},
|
|
54
|
+
modes: ['chat', 'work'],
|
|
55
|
+
isConcurrencySafe: () => false,
|
|
56
|
+
isReadOnly: () => false,
|
|
57
|
+
async execute(input, ctx) {
|
|
58
|
+
const memoryStore = ctx?.memoryStore;
|
|
59
|
+
if (!memoryStore) {
|
|
60
|
+
return JSON.stringify({ error: 'Memory system not initialized' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
switch (input.action) {
|
|
65
|
+
case 'write_entry': {
|
|
66
|
+
if (!input.entry) return JSON.stringify({ error: 'entry is required for "write_entry"' });
|
|
67
|
+
if (!input.entry.name) return JSON.stringify({ error: 'entry.name is required' });
|
|
68
|
+
if (!input.entry.content) return JSON.stringify({ error: 'entry.content is required' });
|
|
69
|
+
|
|
70
|
+
const slug = memoryStore.writeEntry(input.entry);
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
success: true,
|
|
73
|
+
slug,
|
|
74
|
+
message: `Memory entry "${input.entry.name}" saved as ${slug}.md`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'delete_entry': {
|
|
79
|
+
if (!input.name) return JSON.stringify({ error: 'name is required for "delete_entry"' });
|
|
80
|
+
const deleted = memoryStore.deleteEntry(input.name);
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
success: deleted,
|
|
83
|
+
message: deleted
|
|
84
|
+
? `Deleted memory entry "${input.name}"`
|
|
85
|
+
: `Entry "${input.name}" not found`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'write_profile': {
|
|
90
|
+
if (!input.content && input.content !== '') {
|
|
91
|
+
return JSON.stringify({ error: 'content is required for "write_profile"' });
|
|
92
|
+
}
|
|
93
|
+
memoryStore.writeProfile(input.content);
|
|
94
|
+
return JSON.stringify({ success: true, message: 'MEMORY.md updated' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'add_to_section': {
|
|
98
|
+
if (!input.name) return JSON.stringify({ error: 'name (section) is required for "add_to_section"' });
|
|
99
|
+
if (!input.content) return JSON.stringify({ error: 'content (line) is required for "add_to_section"' });
|
|
100
|
+
memoryStore.addToSection(input.name, input.content);
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
success: true,
|
|
103
|
+
message: `Added to section "${input.name}" in MEMORY.md`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
return JSON.stringify({ error: `Unknown action: ${input.action}` });
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return JSON.stringify({ error: `Memory write failed: ${err.message}` });
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notebook-edit.js — Edit Jupyter notebook cells.
|
|
3
|
+
*
|
|
4
|
+
* Reads and modifies .ipynb notebook files by cell index.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineTool } from './types.js';
|
|
8
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
|
|
12
|
+
export default defineTool({
|
|
13
|
+
name: 'NotebookEdit',
|
|
14
|
+
description: `Edit a Jupyter notebook (.ipynb file) cell.
|
|
15
|
+
|
|
16
|
+
Actions:
|
|
17
|
+
- "replace" — replace the source of a cell at the given index
|
|
18
|
+
- "insert" — insert a new cell after the given index
|
|
19
|
+
- "delete" — delete the cell at the given index
|
|
20
|
+
- "read" — read the notebook content (all cells)
|
|
21
|
+
|
|
22
|
+
Cell types: "code" or "markdown"`,
|
|
23
|
+
parameters: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
notebook_path: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Path to the .ipynb file',
|
|
29
|
+
},
|
|
30
|
+
action: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
enum: ['replace', 'insert', 'delete', 'read'],
|
|
33
|
+
description: 'Operation to perform (default: "replace")',
|
|
34
|
+
},
|
|
35
|
+
cell_index: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'Cell index (0-based)',
|
|
38
|
+
},
|
|
39
|
+
cell_type: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['code', 'markdown'],
|
|
42
|
+
description: 'Cell type for insert/replace',
|
|
43
|
+
},
|
|
44
|
+
source: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'New cell source content',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ['notebook_path'],
|
|
50
|
+
},
|
|
51
|
+
modes: ['work'],
|
|
52
|
+
isConcurrencySafe: () => false,
|
|
53
|
+
isReadOnly: (input) => input?.action === 'read',
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
const { notebook_path, action = 'replace', cell_index, cell_type, source } = input;
|
|
56
|
+
if (!notebook_path) return JSON.stringify({ error: 'notebook_path is required' });
|
|
57
|
+
|
|
58
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
59
|
+
const absPath = resolve(cwd, notebook_path);
|
|
60
|
+
|
|
61
|
+
if (!existsSync(absPath)) {
|
|
62
|
+
if (action === 'read') return JSON.stringify({ error: `Notebook not found: ${absPath}` });
|
|
63
|
+
// For write actions on new file, create an empty notebook
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
let notebook;
|
|
68
|
+
if (existsSync(absPath)) {
|
|
69
|
+
const raw = await readFile(absPath, 'utf-8');
|
|
70
|
+
notebook = JSON.parse(raw);
|
|
71
|
+
} else {
|
|
72
|
+
notebook = {
|
|
73
|
+
cells: [],
|
|
74
|
+
metadata: { kernelspec: { display_name: 'Python 3', language: 'python', name: 'python3' } },
|
|
75
|
+
nbformat: 4,
|
|
76
|
+
nbformat_minor: 5,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === 'read') {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
cells: notebook.cells.map((cell, i) => ({
|
|
83
|
+
index: i,
|
|
84
|
+
cell_type: cell.cell_type,
|
|
85
|
+
source: Array.isArray(cell.source) ? cell.source.join('') : cell.source,
|
|
86
|
+
outputs: cell.outputs ? cell.outputs.length : 0,
|
|
87
|
+
})),
|
|
88
|
+
totalCells: notebook.cells.length,
|
|
89
|
+
}, null, 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === 'replace') {
|
|
93
|
+
if (cell_index === undefined) return JSON.stringify({ error: 'cell_index is required for replace' });
|
|
94
|
+
if (source === undefined) return JSON.stringify({ error: 'source is required for replace' });
|
|
95
|
+
if (cell_index < 0 || cell_index >= notebook.cells.length) {
|
|
96
|
+
return JSON.stringify({ error: `Cell index ${cell_index} out of range (0-${notebook.cells.length - 1})` });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
notebook.cells[cell_index].source = source.split('\n').map((l, i, arr) => i < arr.length - 1 ? l + '\n' : l);
|
|
100
|
+
if (cell_type) notebook.cells[cell_index].cell_type = cell_type;
|
|
101
|
+
} else if (action === 'insert') {
|
|
102
|
+
if (source === undefined) return JSON.stringify({ error: 'source is required for insert' });
|
|
103
|
+
const type = cell_type || 'code';
|
|
104
|
+
const newCell = {
|
|
105
|
+
cell_type: type,
|
|
106
|
+
source: source.split('\n').map((l, i, arr) => i < arr.length - 1 ? l + '\n' : l),
|
|
107
|
+
metadata: {},
|
|
108
|
+
...(type === 'code' ? { outputs: [], execution_count: null } : {}),
|
|
109
|
+
};
|
|
110
|
+
const insertIdx = cell_index !== undefined ? cell_index + 1 : notebook.cells.length;
|
|
111
|
+
notebook.cells.splice(insertIdx, 0, newCell);
|
|
112
|
+
} else if (action === 'delete') {
|
|
113
|
+
if (cell_index === undefined) return JSON.stringify({ error: 'cell_index is required for delete' });
|
|
114
|
+
if (cell_index < 0 || cell_index >= notebook.cells.length) {
|
|
115
|
+
return JSON.stringify({ error: `Cell index ${cell_index} out of range` });
|
|
116
|
+
}
|
|
117
|
+
notebook.cells.splice(cell_index, 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await writeFile(absPath, JSON.stringify(notebook, null, 1), 'utf-8');
|
|
121
|
+
|
|
122
|
+
return JSON.stringify({
|
|
123
|
+
success: true,
|
|
124
|
+
action,
|
|
125
|
+
totalCells: notebook.cells.length,
|
|
126
|
+
message: `Notebook ${action}d successfully`,
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return JSON.stringify({ error: `Notebook edit failed: ${err.message}` });
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-permissions.js — Request permission for dangerous operations.
|
|
3
|
+
*
|
|
4
|
+
* When an operation is flagged as destructive, this tool requests
|
|
5
|
+
* explicit user permission before proceeding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
|
|
10
|
+
export default defineTool({
|
|
11
|
+
name: 'RequestPermissions',
|
|
12
|
+
description: `Request permission from the user for a potentially dangerous operation.
|
|
13
|
+
|
|
14
|
+
Use this before executing destructive operations like:
|
|
15
|
+
- Deleting files or directories
|
|
16
|
+
- Running commands that modify system state
|
|
17
|
+
- Force-pushing to git
|
|
18
|
+
- Resetting databases
|
|
19
|
+
|
|
20
|
+
The user must explicitly approve before you proceed.`,
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
operation: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Description of the operation that needs permission',
|
|
27
|
+
},
|
|
28
|
+
reason: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Why this operation is necessary',
|
|
31
|
+
},
|
|
32
|
+
risk_level: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
35
|
+
description: 'Risk level of the operation',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['operation'],
|
|
39
|
+
},
|
|
40
|
+
modes: ['work'],
|
|
41
|
+
isConcurrencySafe: () => false,
|
|
42
|
+
isReadOnly: () => true,
|
|
43
|
+
async execute(input, ctx) {
|
|
44
|
+
const { operation, reason, risk_level = 'medium' } = input;
|
|
45
|
+
if (!operation) return JSON.stringify({ error: 'operation is required' });
|
|
46
|
+
|
|
47
|
+
// In a full integration, this would use the ask_user mechanism
|
|
48
|
+
// to get explicit permission. For now, return a structured request.
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
type: 'permission_request',
|
|
51
|
+
operation,
|
|
52
|
+
reason: reason || 'Operation requires explicit permission',
|
|
53
|
+
riskLevel: risk_level,
|
|
54
|
+
message: `⚠️ Permission required for: ${operation}` +
|
|
55
|
+
(reason ? `\nReason: ${reason}` : '') +
|
|
56
|
+
`\nRisk level: ${risk_level}`,
|
|
57
|
+
hint: 'User must explicitly approve this operation before proceeding.',
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|