@otto-assistant/otto 0.7.17 → 0.7.18
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/dist/commands/permissions.js +153 -2
- package/package.json +1 -1
- package/src/commands/permissions.ts +152 -2
|
@@ -49,6 +49,153 @@ function takePendingPermissionContext(contextHash) {
|
|
|
49
49
|
pendingPermissionContexts.delete(contextHash);
|
|
50
50
|
return ctx;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Render rich tool context from permission metadata for Discord/Telegram.
|
|
54
|
+
* Shows what the agent is actually trying to do — the command, file path,
|
|
55
|
+
* URL, content preview, etc. — so the user can make an informed decision.
|
|
56
|
+
*/
|
|
57
|
+
function renderPermissionContext(permission) {
|
|
58
|
+
const tool = permission.permission.toLowerCase();
|
|
59
|
+
const meta = permission.metadata ?? {};
|
|
60
|
+
// Read a string value from metadata, trying multiple key names
|
|
61
|
+
const getStr = (keys, fallback = '') => {
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
const val = meta[key];
|
|
64
|
+
if (typeof val === 'string' && val.length > 0)
|
|
65
|
+
return val;
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
};
|
|
69
|
+
const truncate = (s, maxLen = 500) => s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s;
|
|
70
|
+
const clipBlock = (s, limit) => s.length > limit ? s.slice(0, limit - 1) + '…' : s;
|
|
71
|
+
switch (tool) {
|
|
72
|
+
case 'bash':
|
|
73
|
+
case 'shell':
|
|
74
|
+
case 'shell_command':
|
|
75
|
+
case 'cmd':
|
|
76
|
+
case 'terminal': {
|
|
77
|
+
const command = getStr(['command', 'cmd', 'script']);
|
|
78
|
+
const description = getStr(['description']);
|
|
79
|
+
if (!command && !description)
|
|
80
|
+
return '';
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (description)
|
|
83
|
+
parts.push(`> *${clipBlock(description, 200)}*`);
|
|
84
|
+
if (command)
|
|
85
|
+
parts.push(`\`\`\`bash\n${clipBlock(command, 800)}\n\`\`\``);
|
|
86
|
+
return parts.join('\n');
|
|
87
|
+
}
|
|
88
|
+
case 'edit':
|
|
89
|
+
case 'multiedit':
|
|
90
|
+
case 'str_replace':
|
|
91
|
+
case 'str_replace_based_edit_tool':
|
|
92
|
+
case 'apply_patch': {
|
|
93
|
+
const filePath = getStr(['path', 'file_path', 'filename', 'filePath', 'file']);
|
|
94
|
+
const oldString = getStr(['old_string', 'oldString', 'changes', 'diff']);
|
|
95
|
+
const newString = getStr(['new_string', 'newString']);
|
|
96
|
+
if (!filePath && !oldString && !newString)
|
|
97
|
+
return '';
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (filePath)
|
|
100
|
+
parts.push(`**File:** \`${filePath}\``);
|
|
101
|
+
if (oldString) {
|
|
102
|
+
parts.push(`**Replace:**\n\`\`\`diff\n- ${clipBlock(oldString, 400)}\n+ ${clipBlock(newString || '', 400)}\n\`\`\``);
|
|
103
|
+
}
|
|
104
|
+
else if (newString) {
|
|
105
|
+
parts.push(`**New content:**\n\`\`\`\n${clipBlock(newString, 600)}\n\`\`\``);
|
|
106
|
+
}
|
|
107
|
+
return parts.join('\n');
|
|
108
|
+
}
|
|
109
|
+
case 'write':
|
|
110
|
+
case 'create':
|
|
111
|
+
case 'file_write': {
|
|
112
|
+
const filePath = getStr(['path', 'file_path', 'filename', 'filePath', 'file']);
|
|
113
|
+
const content = getStr(['content', 'text', 'data']);
|
|
114
|
+
if (!filePath && !content)
|
|
115
|
+
return '';
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (filePath)
|
|
118
|
+
parts.push(`**File:** \`${filePath}\``);
|
|
119
|
+
if (content) {
|
|
120
|
+
parts.push(`\`\`\`\n${clipBlock(content, 600)}\n\`\`\``);
|
|
121
|
+
}
|
|
122
|
+
return parts.join('\n');
|
|
123
|
+
}
|
|
124
|
+
case 'webfetch':
|
|
125
|
+
case 'fetch':
|
|
126
|
+
case 'curl':
|
|
127
|
+
case 'wget': {
|
|
128
|
+
const url = getStr(['url', 'uri', 'endpoint']);
|
|
129
|
+
const method = getStr(['method']) || 'GET';
|
|
130
|
+
if (!url)
|
|
131
|
+
return '';
|
|
132
|
+
return `**URL:** \`${method.toUpperCase()}\` ${url}`;
|
|
133
|
+
}
|
|
134
|
+
case 'read': {
|
|
135
|
+
const filePath = getStr(['filePath', 'file_path', 'path', 'file', 'filename']);
|
|
136
|
+
const parentDir = getStr(['parentDir', 'parent_dir', 'directory']);
|
|
137
|
+
if (!filePath && !parentDir)
|
|
138
|
+
return '';
|
|
139
|
+
const parts = [];
|
|
140
|
+
if (filePath)
|
|
141
|
+
parts.push(`**Reading:** \`${filePath}\``);
|
|
142
|
+
if (parentDir)
|
|
143
|
+
parts.push(`**Directory:** \`${parentDir}\``);
|
|
144
|
+
return parts.join('\n');
|
|
145
|
+
}
|
|
146
|
+
case 'list':
|
|
147
|
+
case 'ls': {
|
|
148
|
+
const listPath = getStr(['path', 'directory', 'filePath']);
|
|
149
|
+
if (!listPath)
|
|
150
|
+
return '';
|
|
151
|
+
return `**Listing:** \`${listPath}\``;
|
|
152
|
+
}
|
|
153
|
+
case 'glob': {
|
|
154
|
+
const pattern = getStr(['pattern', 'glob']);
|
|
155
|
+
if (!pattern)
|
|
156
|
+
return '';
|
|
157
|
+
return `**Pattern:** \`${pattern}\``;
|
|
158
|
+
}
|
|
159
|
+
case 'grep': {
|
|
160
|
+
const pattern = getStr(['pattern', 'query']);
|
|
161
|
+
if (!pattern)
|
|
162
|
+
return '';
|
|
163
|
+
return `**Search:** \`${pattern}\``;
|
|
164
|
+
}
|
|
165
|
+
case 'external_directory': {
|
|
166
|
+
const filepath = getStr(['filepath', 'path', 'directory']);
|
|
167
|
+
const parentDir = getStr(['parentDir', 'parent_dir']);
|
|
168
|
+
const parts = [];
|
|
169
|
+
if (filepath)
|
|
170
|
+
parts.push(`**Path:** \`${filepath}\``);
|
|
171
|
+
if (parentDir)
|
|
172
|
+
parts.push(`**Parent:** \`${parentDir}\``);
|
|
173
|
+
return parts.join('\n');
|
|
174
|
+
}
|
|
175
|
+
case 'task':
|
|
176
|
+
case 'subagent': {
|
|
177
|
+
const description = getStr(['description', 'prompt']);
|
|
178
|
+
if (!description)
|
|
179
|
+
return '';
|
|
180
|
+
return `> ${clipBlock(description, 300)}`;
|
|
181
|
+
}
|
|
182
|
+
default: {
|
|
183
|
+
// Generic: show description or first meaningful metadata field
|
|
184
|
+
const description = getStr(['description', 'action', 'operation', 'command']);
|
|
185
|
+
if (description)
|
|
186
|
+
return `> *${clipBlock(description, 300)}*`;
|
|
187
|
+
const relevantKeys = Object.keys(meta).filter((k) => !['sessionID', 'id', 'type'].includes(k));
|
|
188
|
+
if (relevantKeys.length > 0) {
|
|
189
|
+
const preview = relevantKeys
|
|
190
|
+
.slice(0, 3)
|
|
191
|
+
.map((k) => `${k}: ${String(meta[k]).slice(0, 60)}`)
|
|
192
|
+
.join('\n');
|
|
193
|
+
return `\`\`\`\n${clipBlock(preview, 400)}\n\`\`\``;
|
|
194
|
+
}
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
52
199
|
/**
|
|
53
200
|
* Show permission buttons for a permission request.
|
|
54
201
|
* Displays 3 buttons in a row: Accept, Accept Always, Deny.
|
|
@@ -108,11 +255,13 @@ export async function showPermissionButtons({ thread, permission, directory, per
|
|
|
108
255
|
const externalDirLine = permission.permission === 'external_directory'
|
|
109
256
|
? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
|
|
110
257
|
: '';
|
|
258
|
+
const contextStr = renderPermissionContext(permission);
|
|
111
259
|
const fullContent = `⚠️ **Permission Required**\n` +
|
|
112
260
|
subtaskLine +
|
|
113
261
|
`**Type:** \`${permission.permission}\`\n` +
|
|
114
262
|
externalDirLine +
|
|
115
|
-
(patternStr ? `**Pattern:** \`${patternStr}
|
|
263
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
264
|
+
(contextStr ? `\n${contextStr}` : '');
|
|
116
265
|
const permissionMessage = await thread.send({
|
|
117
266
|
content: fullContent.slice(0, 1900),
|
|
118
267
|
components: [actionRow],
|
|
@@ -133,12 +282,14 @@ function updatePermissionMessage({ context, status, }) {
|
|
|
133
282
|
const externalDirLine = context.permission.permission === 'external_directory'
|
|
134
283
|
? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
|
|
135
284
|
: '';
|
|
285
|
+
const contextStr = renderPermissionContext(context.permission);
|
|
136
286
|
return message.edit({
|
|
137
287
|
content: `⚠️ **Permission Required**\n` +
|
|
138
288
|
`**Type:** \`${context.permission.permission}\`\n` +
|
|
139
289
|
externalDirLine +
|
|
140
290
|
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
141
|
-
|
|
291
|
+
(contextStr ? `\n${contextStr}\n` : '') +
|
|
292
|
+
`\n${status}`,
|
|
142
293
|
components: [],
|
|
143
294
|
});
|
|
144
295
|
})
|
package/package.json
CHANGED
|
@@ -92,6 +92,152 @@ function takePendingPermissionContext(contextHash: string): PendingPermissionCon
|
|
|
92
92
|
return ctx
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Render rich tool context from permission metadata for Discord/Telegram.
|
|
97
|
+
* Shows what the agent is actually trying to do — the command, file path,
|
|
98
|
+
* URL, content preview, etc. — so the user can make an informed decision.
|
|
99
|
+
*/
|
|
100
|
+
function renderPermissionContext(permission: PermissionRequest): string {
|
|
101
|
+
const tool = permission.permission.toLowerCase()
|
|
102
|
+
const meta = permission.metadata ?? {}
|
|
103
|
+
|
|
104
|
+
// Read a string value from metadata, trying multiple key names
|
|
105
|
+
const getStr = (keys: string[], fallback = ''): string => {
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
const val = meta[key]
|
|
108
|
+
if (typeof val === 'string' && val.length > 0) return val
|
|
109
|
+
}
|
|
110
|
+
return fallback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const truncate = (s: string, maxLen = 500): string =>
|
|
114
|
+
s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s
|
|
115
|
+
|
|
116
|
+
const clipBlock = (s: string, limit: number): string =>
|
|
117
|
+
s.length > limit ? s.slice(0, limit - 1) + '…' : s
|
|
118
|
+
|
|
119
|
+
switch (tool) {
|
|
120
|
+
case 'bash':
|
|
121
|
+
case 'shell':
|
|
122
|
+
case 'shell_command':
|
|
123
|
+
case 'cmd':
|
|
124
|
+
case 'terminal': {
|
|
125
|
+
const command = getStr(['command', 'cmd', 'script'])
|
|
126
|
+
const description = getStr(['description'])
|
|
127
|
+
if (!command && !description) return ''
|
|
128
|
+
const parts: string[] = []
|
|
129
|
+
if (description) parts.push(`> *${clipBlock(description, 200)}*`)
|
|
130
|
+
if (command) parts.push(`\`\`\`bash\n${clipBlock(command, 800)}\n\`\`\``)
|
|
131
|
+
return parts.join('\n')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'edit':
|
|
135
|
+
case 'multiedit':
|
|
136
|
+
case 'str_replace':
|
|
137
|
+
case 'str_replace_based_edit_tool':
|
|
138
|
+
case 'apply_patch': {
|
|
139
|
+
const filePath = getStr(['path', 'file_path', 'filename', 'filePath', 'file'])
|
|
140
|
+
const oldString = getStr(['old_string', 'oldString', 'changes', 'diff'])
|
|
141
|
+
const newString = getStr(['new_string', 'newString'])
|
|
142
|
+
if (!filePath && !oldString && !newString) return ''
|
|
143
|
+
const parts: string[] = []
|
|
144
|
+
if (filePath) parts.push(`**File:** \`${filePath}\``)
|
|
145
|
+
if (oldString) {
|
|
146
|
+
parts.push(`**Replace:**\n\`\`\`diff\n- ${clipBlock(oldString, 400)}\n+ ${clipBlock(newString || '', 400)}\n\`\`\``)
|
|
147
|
+
} else if (newString) {
|
|
148
|
+
parts.push(`**New content:**\n\`\`\`\n${clipBlock(newString, 600)}\n\`\`\``)
|
|
149
|
+
}
|
|
150
|
+
return parts.join('\n')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'write':
|
|
154
|
+
case 'create':
|
|
155
|
+
case 'file_write': {
|
|
156
|
+
const filePath = getStr(['path', 'file_path', 'filename', 'filePath', 'file'])
|
|
157
|
+
const content = getStr(['content', 'text', 'data'])
|
|
158
|
+
if (!filePath && !content) return ''
|
|
159
|
+
const parts: string[] = []
|
|
160
|
+
if (filePath) parts.push(`**File:** \`${filePath}\``)
|
|
161
|
+
if (content) {
|
|
162
|
+
parts.push(`\`\`\`\n${clipBlock(content, 600)}\n\`\`\``)
|
|
163
|
+
}
|
|
164
|
+
return parts.join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'webfetch':
|
|
168
|
+
case 'fetch':
|
|
169
|
+
case 'curl':
|
|
170
|
+
case 'wget': {
|
|
171
|
+
const url = getStr(['url', 'uri', 'endpoint'])
|
|
172
|
+
const method = getStr(['method']) || 'GET'
|
|
173
|
+
if (!url) return ''
|
|
174
|
+
return `**URL:** \`${method.toUpperCase()}\` ${url}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'read': {
|
|
178
|
+
const filePath = getStr(['filePath', 'file_path', 'path', 'file', 'filename'])
|
|
179
|
+
const parentDir = getStr(['parentDir', 'parent_dir', 'directory'])
|
|
180
|
+
if (!filePath && !parentDir) return ''
|
|
181
|
+
const parts: string[] = []
|
|
182
|
+
if (filePath) parts.push(`**Reading:** \`${filePath}\``)
|
|
183
|
+
if (parentDir) parts.push(`**Directory:** \`${parentDir}\``)
|
|
184
|
+
return parts.join('\n')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'list':
|
|
188
|
+
case 'ls': {
|
|
189
|
+
const listPath = getStr(['path', 'directory', 'filePath'])
|
|
190
|
+
if (!listPath) return ''
|
|
191
|
+
return `**Listing:** \`${listPath}\``
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'glob': {
|
|
195
|
+
const pattern = getStr(['pattern', 'glob'])
|
|
196
|
+
if (!pattern) return ''
|
|
197
|
+
return `**Pattern:** \`${pattern}\``
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'grep': {
|
|
201
|
+
const pattern = getStr(['pattern', 'query'])
|
|
202
|
+
if (!pattern) return ''
|
|
203
|
+
return `**Search:** \`${pattern}\``
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case 'external_directory': {
|
|
207
|
+
const filepath = getStr(['filepath', 'path', 'directory'])
|
|
208
|
+
const parentDir = getStr(['parentDir', 'parent_dir'])
|
|
209
|
+
const parts: string[] = []
|
|
210
|
+
if (filepath) parts.push(`**Path:** \`${filepath}\``)
|
|
211
|
+
if (parentDir) parts.push(`**Parent:** \`${parentDir}\``)
|
|
212
|
+
return parts.join('\n')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case 'task':
|
|
216
|
+
case 'subagent': {
|
|
217
|
+
const description = getStr(['description', 'prompt'])
|
|
218
|
+
if (!description) return ''
|
|
219
|
+
return `> ${clipBlock(description, 300)}`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
default: {
|
|
223
|
+
// Generic: show description or first meaningful metadata field
|
|
224
|
+
const description = getStr(['description', 'action', 'operation', 'command'])
|
|
225
|
+
if (description) return `> *${clipBlock(description, 300)}*`
|
|
226
|
+
const relevantKeys = Object.keys(meta).filter(
|
|
227
|
+
(k) => !['sessionID', 'id', 'type'].includes(k),
|
|
228
|
+
)
|
|
229
|
+
if (relevantKeys.length > 0) {
|
|
230
|
+
const preview = relevantKeys
|
|
231
|
+
.slice(0, 3)
|
|
232
|
+
.map((k) => `${k}: ${String(meta[k]).slice(0, 60)}`)
|
|
233
|
+
.join('\n')
|
|
234
|
+
return `\`\`\`\n${clipBlock(preview, 400)}\n\`\`\``
|
|
235
|
+
}
|
|
236
|
+
return ''
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
95
241
|
/**
|
|
96
242
|
* Show permission buttons for a permission request.
|
|
97
243
|
* Displays 3 buttons in a row: Accept, Accept Always, Deny.
|
|
@@ -178,12 +324,14 @@ export async function showPermissionButtons({
|
|
|
178
324
|
permission.permission === 'external_directory'
|
|
179
325
|
? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
|
|
180
326
|
: ''
|
|
327
|
+
const contextStr = renderPermissionContext(permission)
|
|
181
328
|
const fullContent =
|
|
182
329
|
`⚠️ **Permission Required**\n` +
|
|
183
330
|
subtaskLine +
|
|
184
331
|
`**Type:** \`${permission.permission}\`\n` +
|
|
185
332
|
externalDirLine +
|
|
186
|
-
(patternStr ? `**Pattern:** \`${patternStr}
|
|
333
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
334
|
+
(contextStr ? `\n${contextStr}` : '')
|
|
187
335
|
const permissionMessage = await thread.send({
|
|
188
336
|
content: fullContent.slice(0, 1900),
|
|
189
337
|
components: [actionRow],
|
|
@@ -215,13 +363,15 @@ function updatePermissionMessage({
|
|
|
215
363
|
context.permission.permission === 'external_directory'
|
|
216
364
|
? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
|
|
217
365
|
: ''
|
|
366
|
+
const contextStr = renderPermissionContext(context.permission)
|
|
218
367
|
return message.edit({
|
|
219
368
|
content:
|
|
220
369
|
`⚠️ **Permission Required**\n` +
|
|
221
370
|
`**Type:** \`${context.permission.permission}\`\n` +
|
|
222
371
|
externalDirLine +
|
|
223
372
|
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
224
|
-
|
|
373
|
+
(contextStr ? `\n${contextStr}\n` : '') +
|
|
374
|
+
`\n${status}`,
|
|
225
375
|
components: [],
|
|
226
376
|
})
|
|
227
377
|
})
|