atris 3.11.0 → 3.12.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.
- package/atris/skills/atris-feedback/SKILL.md +108 -0
- package/bin/atris.js +5 -2
- package/commands/feedback.js +223 -71
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atris-feedback
|
|
3
|
+
description: Submit, list, resolve, close, or delete Atris customer feedback. Use when user types /feedback or asks to triage the feedback queue.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- feedback
|
|
7
|
+
- customer
|
|
8
|
+
- admin
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Feedback
|
|
12
|
+
|
|
13
|
+
One skill for everything: submit feedback, view the queue, resolve/close/delete items.
|
|
14
|
+
|
|
15
|
+
## Parse the input
|
|
16
|
+
|
|
17
|
+
- `/feedback` (no args) → show the queue
|
|
18
|
+
- `/feedback <message>` → submit new feedback
|
|
19
|
+
- `/feedback resolve <id> <resolution>` → mark as resolved, notify customer
|
|
20
|
+
- `/feedback close <id>` → close as wontfix/duplicate
|
|
21
|
+
- `/feedback delete <id>` → remove from queue
|
|
22
|
+
|
|
23
|
+
## Preferred path: the Atris CLI
|
|
24
|
+
|
|
25
|
+
The `atris` CLI wraps every feedback operation against the production API and
|
|
26
|
+
handles auth from the user's login. Use it first — it's the canonical,
|
|
27
|
+
audited path and works without needing AWS credentials.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
atris feedback # list queue
|
|
31
|
+
atris feedback "the calendar hangs" # submit
|
|
32
|
+
atris feedback resolve abc123 "fixed" # mark resolved
|
|
33
|
+
atris feedback close abc123 # close as wontfix
|
|
34
|
+
atris feedback delete abc123 # delete
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
IDs can be short (first 8 chars of the UUID) — the CLI resolves the prefix
|
|
38
|
+
against the live list before acting.
|
|
39
|
+
|
|
40
|
+
If `atris` is not on PATH, it lives at `~/arena/atris-cli/bin/atris.js`.
|
|
41
|
+
|
|
42
|
+
## Fallback: direct API / DynamoDB
|
|
43
|
+
|
|
44
|
+
Only use this path if the CLI is unavailable (stale install, broken login).
|
|
45
|
+
|
|
46
|
+
### Setup
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd ~/arena/atrisos-backend && source venv/bin/activate
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from dotenv import load_dotenv; load_dotenv('backend/.env')
|
|
54
|
+
import boto3
|
|
55
|
+
table = boto3.resource('dynamodb', region_name='us-east-1').Table('atris_feedback')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Submit
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl -s -X POST "https://api.atris.ai/api/feedback" \
|
|
62
|
+
-H "Content-Type: application/json" \
|
|
63
|
+
-H "X-Feedback-Key: $FEEDBACK_API_KEY" \
|
|
64
|
+
-d '{"message": "THE_MESSAGE", "source": "cli", "context": {"user_email": "GIT_EMAIL"}}'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Confirm: `Feedback submitted (id: abc123)`
|
|
68
|
+
|
|
69
|
+
### Resolve
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
table.update_item(
|
|
73
|
+
Key={'id': FULL_ID},
|
|
74
|
+
UpdateExpression='SET #s = :s, resolution = :r, resolved_at = :t',
|
|
75
|
+
ExpressionAttributeNames={'#s': 'status'},
|
|
76
|
+
ExpressionAttributeValues={':s': 'resolved', ':r': RESOLUTION, ':t': NOW},
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Print: `Resolved abc123: <resolution>`
|
|
81
|
+
|
|
82
|
+
### Close
|
|
83
|
+
|
|
84
|
+
Same as resolve but `status = 'closed'`, no resolution text needed.
|
|
85
|
+
|
|
86
|
+
### Delete
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
table.delete_item(Key={'id': FULL_ID})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Print: `Deleted abc123`
|
|
93
|
+
|
|
94
|
+
### ID matching
|
|
95
|
+
|
|
96
|
+
Users type short IDs (first 8 chars). Scan the table and match by prefix
|
|
97
|
+
to find the full UUID.
|
|
98
|
+
|
|
99
|
+
## Security
|
|
100
|
+
|
|
101
|
+
- NEVER include API keys, tokens, or secrets in feedback messages
|
|
102
|
+
- Server-side sanitization strips them anyway (double protection)
|
|
103
|
+
- Max 2000 chars per message
|
|
104
|
+
|
|
105
|
+
## Output
|
|
106
|
+
|
|
107
|
+
Always print the result directly as text. Never leave it inside a tool
|
|
108
|
+
call expansion.
|
package/bin/atris.js
CHANGED
|
@@ -328,8 +328,11 @@ function showHelp() {
|
|
|
328
328
|
console.log(' plugin info - Preview what will be included');
|
|
329
329
|
console.log('');
|
|
330
330
|
console.log('Feedback:');
|
|
331
|
-
console.log(' feedback "msg"
|
|
332
|
-
console.log(' feedback
|
|
331
|
+
console.log(' feedback "msg" - Submit feedback');
|
|
332
|
+
console.log(' feedback - List feedback queue');
|
|
333
|
+
console.log(' feedback resolve <id> "<note>" - Mark resolved (admin)');
|
|
334
|
+
console.log(' feedback close <id> - Close as wontfix (admin)');
|
|
335
|
+
console.log(' feedback delete <id> - Delete feedback (admin)');
|
|
333
336
|
console.log('');
|
|
334
337
|
console.log('Other:');
|
|
335
338
|
console.log(' version - Show Atris version');
|
package/commands/feedback.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Feedback command for Atris CLI
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* atris feedback "message here"
|
|
6
|
-
* atris feedback
|
|
7
|
-
* atris feedback list
|
|
5
|
+
* atris feedback "message here" Submit feedback
|
|
6
|
+
* atris feedback List feedback
|
|
7
|
+
* atris feedback list List feedback
|
|
8
|
+
* atris feedback resolve <id> "<resolution>" Mark resolved (admin)
|
|
9
|
+
* atris feedback close <id> Close as wontfix (admin)
|
|
10
|
+
* atris feedback delete <id> Delete feedback (admin)
|
|
11
|
+
*
|
|
12
|
+
* IDs may be the first 8 chars of the UUID — the CLI resolves the prefix
|
|
13
|
+
* against the live list before making the write request.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
const fs = require('fs');
|
|
@@ -21,40 +27,17 @@ function getAuth() {
|
|
|
21
27
|
return { token: creds.token, email: creds.email || 'unknown' };
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
function
|
|
25
|
-
// 1. Check .atris/business.json in current directory
|
|
26
|
-
const bizFile = path.join(process.cwd(), '.atris', 'business.json');
|
|
27
|
-
if (fs.existsSync(bizFile)) {
|
|
28
|
-
try {
|
|
29
|
-
const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
|
|
30
|
-
if (biz.business_id) return biz.business_id;
|
|
31
|
-
} catch {}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 2. Check ~/.atris/businesses.json (first connected business)
|
|
35
|
-
const home = require('os').homedir();
|
|
36
|
-
const globalBizFile = path.join(home, '.atris', 'businesses.json');
|
|
37
|
-
if (fs.existsSync(globalBizFile)) {
|
|
38
|
-
try {
|
|
39
|
-
const businesses = JSON.parse(fs.readFileSync(globalBizFile, 'utf8'));
|
|
40
|
-
const slugs = Object.keys(businesses);
|
|
41
|
-
if (slugs.length > 0 && businesses[slugs[0]].business_id) {
|
|
42
|
-
return businesses[slugs[0]].business_id;
|
|
43
|
-
}
|
|
44
|
-
} catch {}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function submitFeedback(message) {
|
|
30
|
+
async function submitFeedback(message, opts = {}) {
|
|
51
31
|
if (!message) {
|
|
52
|
-
console.error('Usage: atris feedback "your message here"');
|
|
32
|
+
console.error('Usage: atris feedback "your message here" [--business <slug|id>]');
|
|
53
33
|
process.exit(1);
|
|
54
34
|
}
|
|
55
35
|
|
|
56
36
|
const { token } = getAuth();
|
|
57
|
-
|
|
37
|
+
// Only attach business_id when the user explicitly asked for it via --business.
|
|
38
|
+
// Auto-scoping to "first business in businesses.json" silently hid feedback
|
|
39
|
+
// from every other workspace the user belongs to.
|
|
40
|
+
const businessId = opts.businessId || null;
|
|
58
41
|
|
|
59
42
|
const body = {
|
|
60
43
|
message,
|
|
@@ -81,70 +64,239 @@ async function submitFeedback(message) {
|
|
|
81
64
|
}
|
|
82
65
|
}
|
|
83
66
|
|
|
67
|
+
async function fetchFeedbackItems({ token, businessId, limit = 100, status = null } = {}) {
|
|
68
|
+
let url = `/feedback?limit=${limit}`;
|
|
69
|
+
if (businessId) url += `&business_id=${businessId}`;
|
|
70
|
+
if (status) url += `&status=${encodeURIComponent(status)}`;
|
|
71
|
+
|
|
72
|
+
const result = await apiRequestJson(url, { method: 'GET', token });
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
console.error(`Error: ${result.error || 'Failed to fetch feedback'}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
return result.data?.feedback || [];
|
|
78
|
+
}
|
|
79
|
+
|
|
84
80
|
async function listFeedback() {
|
|
85
81
|
const { token } = getAuth();
|
|
86
|
-
|
|
82
|
+
// Do NOT auto-scope by business: admins expect to see the full queue.
|
|
83
|
+
// The API already scopes non-admins to their own businesses server-side.
|
|
84
|
+
const items = await fetchFeedbackItems({ token, limit: 20 });
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
if (items.length === 0) {
|
|
87
|
+
console.log('No feedback found.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`Feedback Queue (${items.length} item${items.length !== 1 ? 's' : ''})\n`);
|
|
92
|
+
|
|
93
|
+
items.forEach((item, idx) => {
|
|
94
|
+
const status = (item.status || 'open').toUpperCase();
|
|
95
|
+
const shortId = (item.id || '').substring(0, 8);
|
|
96
|
+
const msg = item.message || '';
|
|
97
|
+
const preview = msg.length > 120 ? msg.substring(0, 120) + '...' : msg;
|
|
98
|
+
const date = item.created_at
|
|
99
|
+
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
100
|
+
: '';
|
|
101
|
+
const fromEmail = item.context?.user_email || item.user_id || '';
|
|
102
|
+
|
|
103
|
+
console.log(`${idx + 1}. [${status}] id:${shortId}${date ? ' ' + date : ''}`);
|
|
104
|
+
console.log(` "${preview}"`);
|
|
105
|
+
if (fromEmail) console.log(` from: ${fromEmail}`);
|
|
106
|
+
if (item.resolution) console.log(` resolution: ${item.resolution}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve a short ID prefix (or full UUID) to the full UUID by scanning
|
|
113
|
+
* the list endpoint. Returns null if no unique match found.
|
|
114
|
+
*/
|
|
115
|
+
async function resolveIdPrefix(prefix, { token, businessId }) {
|
|
116
|
+
if (!prefix) return { error: 'ID required' };
|
|
117
|
+
// If it looks like a full UUID, trust it
|
|
118
|
+
if (prefix.length >= 32) return { id: prefix };
|
|
119
|
+
|
|
120
|
+
const items = await fetchFeedbackItems({ token, businessId, limit: 200 });
|
|
121
|
+
const matches = items.filter(it => (it.id || '').startsWith(prefix));
|
|
122
|
+
|
|
123
|
+
if (matches.length === 0) return { error: `No feedback matches id prefix "${prefix}"` };
|
|
124
|
+
if (matches.length > 1) {
|
|
125
|
+
return {
|
|
126
|
+
error: `Ambiguous id "${prefix}" — matches ${matches.length} items. Use a longer prefix.`,
|
|
127
|
+
};
|
|
91
128
|
}
|
|
129
|
+
return { id: matches[0].id, item: matches[0] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function resolveFeedback(idPrefix, resolution) {
|
|
133
|
+
if (!idPrefix || !resolution) {
|
|
134
|
+
console.error('Usage: atris feedback resolve <id> "<resolution>"');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const { token } = getAuth();
|
|
92
138
|
|
|
93
|
-
const
|
|
94
|
-
|
|
139
|
+
const lookup = await resolveIdPrefix(idPrefix, { token });
|
|
140
|
+
if (lookup.error) {
|
|
141
|
+
console.error(`Error: ${lookup.error}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await apiRequestJson(`/feedback/${lookup.id}`, {
|
|
146
|
+
method: 'PATCH',
|
|
95
147
|
token,
|
|
148
|
+
body: { status: 'resolved', resolution },
|
|
96
149
|
});
|
|
97
150
|
|
|
98
151
|
if (!result.ok) {
|
|
99
|
-
console.error(`Error: ${result.error || 'Failed to
|
|
152
|
+
console.error(`Error: ${result.error || 'Failed to resolve feedback'}`);
|
|
100
153
|
process.exit(1);
|
|
101
154
|
}
|
|
155
|
+
console.log(`Resolved ${lookup.id.substring(0, 8)}: ${resolution}`);
|
|
156
|
+
}
|
|
102
157
|
|
|
103
|
-
|
|
158
|
+
async function closeFeedback(idPrefix) {
|
|
159
|
+
if (!idPrefix) {
|
|
160
|
+
console.error('Usage: atris feedback close <id>');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const { token } = getAuth();
|
|
104
164
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
const lookup = await resolveIdPrefix(idPrefix, { token });
|
|
166
|
+
if (lookup.error) {
|
|
167
|
+
console.error(`Error: ${lookup.error}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = await apiRequestJson(`/feedback/${lookup.id}`, {
|
|
172
|
+
method: 'PATCH',
|
|
173
|
+
token,
|
|
174
|
+
body: { status: 'closed' },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!result.ok) {
|
|
178
|
+
console.error(`Error: ${result.error || 'Failed to close feedback'}`);
|
|
179
|
+
process.exit(1);
|
|
108
180
|
}
|
|
181
|
+
console.log(`Closed ${lookup.id.substring(0, 8)}`);
|
|
182
|
+
}
|
|
109
183
|
|
|
110
|
-
|
|
184
|
+
async function deleteFeedback(idPrefix) {
|
|
185
|
+
if (!idPrefix) {
|
|
186
|
+
console.error('Usage: atris feedback delete <id>');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
const { token } = getAuth();
|
|
111
190
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
191
|
+
const lookup = await resolveIdPrefix(idPrefix, { token });
|
|
192
|
+
if (lookup.error) {
|
|
193
|
+
console.error(`Error: ${lookup.error}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await apiRequestJson(`/feedback/${lookup.id}`, {
|
|
198
|
+
method: 'DELETE',
|
|
199
|
+
token,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
console.error(`Error: ${result.error || 'Failed to delete feedback'}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
console.log(`Deleted ${lookup.id.substring(0, 8)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printHelp() {
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log('Usage:');
|
|
212
|
+
console.log(' atris feedback "message" Submit feedback (global)');
|
|
213
|
+
console.log(' atris feedback "msg" --business <slug> Submit feedback tagged to a business');
|
|
214
|
+
console.log(' atris feedback List feedback');
|
|
215
|
+
console.log(' atris feedback list List feedback');
|
|
216
|
+
console.log(' atris feedback resolve <id> "<note>" Mark resolved (admin)');
|
|
217
|
+
console.log(' atris feedback close <id> Close as wontfix (admin)');
|
|
218
|
+
console.log(' atris feedback delete <id> Delete feedback (admin)');
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log('IDs may be the first 8 chars of the UUID.');
|
|
221
|
+
console.log('Business slugs come from ~/.atris/businesses.json (e.g. pallet, atris-labs-1).');
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveBusinessArg(value) {
|
|
226
|
+
if (!value) return null;
|
|
227
|
+
// Full UUID — trust it
|
|
228
|
+
if (/^[0-9a-f-]{32,}$/i.test(value)) return value;
|
|
229
|
+
// Otherwise treat as slug and look up in ~/.atris/businesses.json
|
|
230
|
+
const home = require('os').homedir();
|
|
231
|
+
const file = path.join(home, '.atris', 'businesses.json');
|
|
232
|
+
if (!fs.existsSync(file)) return null;
|
|
233
|
+
try {
|
|
234
|
+
const map = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
235
|
+
const hit = map[value] || Object.values(map).find(b => b.slug === value);
|
|
236
|
+
return hit ? hit.business_id : null;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function extractFlag(args, ...names) {
|
|
243
|
+
// Returns [value, remainingArgs]. Supports "--flag val" and "--flag=val".
|
|
244
|
+
const remaining = [];
|
|
245
|
+
let value = null;
|
|
246
|
+
for (let i = 0; i < args.length; i++) {
|
|
247
|
+
const a = args[i];
|
|
248
|
+
const eq = a.indexOf('=');
|
|
249
|
+
const key = eq >= 0 ? a.slice(0, eq) : a;
|
|
250
|
+
if (names.includes(key)) {
|
|
251
|
+
value = eq >= 0 ? a.slice(eq + 1) : args[++i];
|
|
252
|
+
} else {
|
|
253
|
+
remaining.push(a);
|
|
126
254
|
}
|
|
127
|
-
console.log('');
|
|
128
255
|
}
|
|
256
|
+
return [value, remaining];
|
|
129
257
|
}
|
|
130
258
|
|
|
131
259
|
async function feedbackCommand() {
|
|
132
|
-
const
|
|
260
|
+
const rawArgs = process.argv.slice(3);
|
|
261
|
+
const [businessArg, args] = extractFlag(rawArgs, '--business', '-b');
|
|
262
|
+
const businessId = resolveBusinessArg(businessArg);
|
|
263
|
+
if (businessArg && !businessId) {
|
|
264
|
+
console.error(`Error: unknown business "${businessArg}". Check ~/.atris/businesses.json`);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const subcommand = args[0];
|
|
133
269
|
|
|
134
270
|
if (!subcommand || subcommand === 'list') {
|
|
135
271
|
await listFeedback();
|
|
136
|
-
|
|
137
|
-
console.log('');
|
|
138
|
-
console.log('Usage:');
|
|
139
|
-
console.log(' atris feedback "message" Submit feedback');
|
|
140
|
-
console.log(' atris feedback List your feedback');
|
|
141
|
-
console.log(' atris feedback list List your feedback');
|
|
142
|
-
console.log('');
|
|
143
|
-
} else {
|
|
144
|
-
// Everything else is a feedback message
|
|
145
|
-
const message = process.argv.slice(3).join(' ');
|
|
146
|
-
await submitFeedback(message);
|
|
272
|
+
return;
|
|
147
273
|
}
|
|
274
|
+
|
|
275
|
+
if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
276
|
+
printHelp();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (subcommand === 'resolve') {
|
|
281
|
+
const id = args[1];
|
|
282
|
+
const resolution = args.slice(2).join(' ');
|
|
283
|
+
await resolveFeedback(id, resolution);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (subcommand === 'close') {
|
|
288
|
+
await closeFeedback(args[1]);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (subcommand === 'delete') {
|
|
293
|
+
await deleteFeedback(args[1]);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Everything else is a feedback message
|
|
298
|
+
const message = args.join(' ');
|
|
299
|
+
await submitFeedback(message, { businessId });
|
|
148
300
|
}
|
|
149
301
|
|
|
150
302
|
module.exports = { feedbackCommand };
|