@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.
@@ -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
+ });