atris 3.12.0 → 3.13.0

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.
@@ -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 };
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function readBusinessBinding(cwd = process.cwd()) {
5
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
6
+ if (!fs.existsSync(bindingPath)) return null;
7
+ try {
8
+ return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ function slugify(value) {
15
+ return String(value || 'business-workflow')
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '') || 'business-workflow';
19
+ }
20
+
21
+ function ensureDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function writeJson(filePath, value) {
26
+ ensureDir(path.dirname(filePath));
27
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
28
+ }
29
+
30
+ function doctor(cwd = process.cwd()) {
31
+ const binding = readBusinessBinding(cwd);
32
+ console.log('Receipt check');
33
+ console.log(`business binding: ${binding ? `${binding.name || binding.slug || binding.business_id} ready` : 'missing'}`);
34
+ console.log(`receipt folder: ${fs.existsSync(path.join(cwd, '.atris', 'receipts')) ? 'ready' : 'missing'}`);
35
+ console.log('');
36
+ console.log('Next: atris receipt init business-workflow');
37
+ }
38
+
39
+ function init(taskSlug = 'business-workflow', cwd = process.cwd()) {
40
+ const binding = readBusinessBinding(cwd);
41
+ if (!binding) {
42
+ console.error('No business binding found. Run: atris business init <name> --here');
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const slug = slugify(taskSlug);
48
+ const root = path.join(cwd, '.atris');
49
+ const taskPath = path.join(root, 'tasks', `${slug}.json`);
50
+ const receiptsDir = path.join(root, 'receipts');
51
+
52
+ ensureDir(receiptsDir);
53
+ fs.writeFileSync(path.join(receiptsDir, '.gitkeep'), '');
54
+ writeJson(taskPath, {
55
+ schema: 'atris.receipt_task.v1',
56
+ slug,
57
+ goal: 'Run one business-computer task and save what happened.',
58
+ workspace: {
59
+ business_id: binding.business_id || binding.id || null,
60
+ workspace_id: binding.workspace_id || null,
61
+ name: binding.name || null,
62
+ slug: binding.slug || null,
63
+ },
64
+ runtime: {
65
+ proof_command: 'atris computer proof',
66
+ replay_command: 'atris experiments replay endstate',
67
+ },
68
+ verify: [
69
+ 'atris computer proof',
70
+ 'atris experiments replay endstate',
71
+ ],
72
+ });
73
+
74
+ console.log(`Receipt task ready: ${slug}`);
75
+ console.log(`Task: ${path.relative(cwd, taskPath)}`);
76
+ console.log(`Receipts: ${path.relative(cwd, receiptsDir)}`);
77
+ }
78
+
79
+ function run(args = []) {
80
+ const dryRun = args.includes('--dry-run');
81
+ console.log('Receipt run');
82
+ console.log('1. atris computer proof');
83
+ console.log('2. atris experiments replay endstate');
84
+ if (dryRun) {
85
+ console.log('Dry run only; no receipts written.');
86
+ return;
87
+ }
88
+ console.log('Run those commands, then save the receipt under .atris/receipts/.');
89
+ }
90
+
91
+ function proofCommand(subcommand = 'doctor', ...args) {
92
+ switch (subcommand || 'doctor') {
93
+ case 'doctor':
94
+ return doctor();
95
+ case 'init':
96
+ return init(args[0] || 'business-workflow');
97
+ case 'proof':
98
+ return run(args);
99
+ case 'help':
100
+ case '--help':
101
+ case '-h':
102
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
103
+ return;
104
+ case 'run':
105
+ return run(args);
106
+ default:
107
+ console.error(`Unknown receipt command: ${subcommand}`);
108
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
109
+ process.exitCode = 1;
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ proofCommand,
115
+ };
package/commands/pull.js CHANGED
@@ -703,8 +703,10 @@ async function pullBusiness(slug) {
703
703
  name: businessName,
704
704
  }, null, 2));
705
705
 
706
- // Wire skills → .claude/skills/ so they work as slash commands
707
- const skillsDir = path.join(outputDir, 'skills');
706
+ // Wire skills → .claude/skills/ so they work as slash commands.
707
+ // Source of truth is atris/skills/ (vendor-neutral, syncs to cloud).
708
+ // .claude/skills/ is a locally-generated adapter Claude Code reads from.
709
+ const skillsDir = path.join(outputDir, 'atris', 'skills');
708
710
  const claudeSkillsDir = path.join(outputDir, '.claude', 'skills');
709
711
 
710
712
  if (fs.existsSync(skillsDir)) {
package/commands/push.js CHANGED
@@ -367,6 +367,12 @@ async function pushAtris() {
367
367
 
368
368
  if (!result.ok) {
369
369
  if (result.status === 403) {
370
+ const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
371
+ if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
372
+ console.error(`\n Access denied: ${detail}`);
373
+ await emit('access_denied', { error_detail: detail });
374
+ process.exit(1);
375
+ }
370
376
  // Permission denied — retry with only team/ and journal/ files
371
377
  const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
372
378
  skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));