atris 3.12.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.
@@ -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" - Submit feedback');
332
- console.log(' feedback - List your 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');
@@ -2,9 +2,15 @@
2
2
  * Feedback command for Atris CLI
3
3
  *
4
4
  * Usage:
5
- * atris feedback "message here" - Submit feedback
6
- * atris feedback - List your feedback
7
- * atris feedback list - List your feedback
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 getBusinessId() {
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
- const businessId = getBusinessId();
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
- const businessId = getBusinessId();
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
- let url = '/feedback?limit=20';
89
- if (businessId) {
90
- url += `&business_id=${businessId}`;
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 result = await apiRequestJson(url, {
94
- method: 'GET',
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 fetch feedback'}`);
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
- const items = result.data?.feedback || [];
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
- if (items.length === 0) {
106
- console.log('No feedback found.');
107
- return;
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
- console.log(`${items.length} feedback item${items.length !== 1 ? 's' : ''}:\n`);
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
- for (const item of items) {
113
- const date = item.created_at
114
- ? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
115
- : '';
116
- const status = item.status || 'open';
117
- const msg = item.message || '';
118
- const preview = msg.length > 80 ? msg.substring(0, 80) + '...' : msg;
119
-
120
- console.log(` [${status}] ${preview}`);
121
- if (date || item.id) {
122
- const parts = [];
123
- if (date) parts.push(date);
124
- if (item.id) parts.push(item.id.substring(0, 8));
125
- console.log(` ${parts.join(' ')}`);
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 subcommand = process.argv[3];
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
- } else if (subcommand === '--help' || subcommand === '-h') {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {