atris 3.52.0 → 3.55.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.
- package/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +8 -7
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +89 -40
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/ax +592 -91
- package/bin/atris +21 -0
- package/bin/atris.js +580 -378
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/computer.js +2 -2
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +399 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +339 -74
- package/commands/now.js +25 -6
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +96 -40
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +418 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -33
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +10 -2
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-ledger-compact.js +127 -0
- package/lib/mission-root.js +4 -2
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/noninteractive.js +88 -0
- package/lib/permission-grants.js +10 -1
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/runner-command.js +5 -4
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +22 -2
- package/lib/task-proof.js +75 -9
- package/lib/task-receipt.js +5 -1
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +4 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/scripts/outbound-artifact-gate.js +227 -0
- package/utils/update-check.js +15 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social commands for Atris CLI — find people, add friends, send messages.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* atris social My social overview (stats, unread, suggestions)
|
|
6
|
+
* atris people <query> Search for people on Atris
|
|
7
|
+
* atris follow <who> Follow someone (username, email, or name)
|
|
8
|
+
* atris unfollow <who> Unfollow someone
|
|
9
|
+
* atris friends Mutual follows (your friends)
|
|
10
|
+
* atris friends followers People who follow you
|
|
11
|
+
* atris friends following People you follow
|
|
12
|
+
* atris msg <who> <text...> Send a direct message
|
|
13
|
+
* atris msg <who> Read the conversation (marks it read)
|
|
14
|
+
* atris inbox Your conversations, unread first
|
|
15
|
+
* atris invite Create an invite link to share
|
|
16
|
+
*
|
|
17
|
+
* <who> resolution: exact UUID passes through; anything else is searched and
|
|
18
|
+
* matched by username, then email, then name. Ambiguous matches list the
|
|
19
|
+
* candidates instead of guessing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { loadCredentials } = require('../utils/auth');
|
|
23
|
+
const { apiRequestJson, getAppBaseUrl } = require('../utils/api');
|
|
24
|
+
const { isHelpToken, argsWantHelp } = require('../lib/noninteractive');
|
|
25
|
+
|
|
26
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
|
+
|
|
28
|
+
function getToken() {
|
|
29
|
+
const creds = loadCredentials();
|
|
30
|
+
if (!creds || !creds.token) {
|
|
31
|
+
console.error('Not logged in. Run: atris login');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
return creds.token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fail(message) {
|
|
38
|
+
console.error(message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function showUsageAndExit(usage) {
|
|
43
|
+
console.error(usage);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function apiErrorMessage(res, fallback) {
|
|
48
|
+
const detail = res && res.data && (res.data.detail || res.data.error);
|
|
49
|
+
if (typeof detail === 'string') return detail;
|
|
50
|
+
if (detail && detail.error && detail.error.message) return detail.error.message;
|
|
51
|
+
if (detail && detail.message) return detail.message;
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function api(pathname, options = {}) {
|
|
56
|
+
const token = getToken();
|
|
57
|
+
const res = await apiRequestJson(pathname, { ...options, token });
|
|
58
|
+
if (res.status === 401) {
|
|
59
|
+
fail('Session expired. Run: atris login');
|
|
60
|
+
}
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function displayName(user) {
|
|
65
|
+
const name = user.name || user.display_name || null;
|
|
66
|
+
const handle = user.username ? `@${user.username}` : null;
|
|
67
|
+
if (name && handle) return `${name} (${handle})`;
|
|
68
|
+
return name || handle || user.email || user.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function userLine(user) {
|
|
72
|
+
const marks = [];
|
|
73
|
+
if (user.is_mutual) marks.push('friends');
|
|
74
|
+
else if (user.is_following) marks.push('following');
|
|
75
|
+
const suffix = marks.length ? ` · ${marks.join(', ')}` : '';
|
|
76
|
+
const bio = user.bio ? `\n ${String(user.bio).slice(0, 100)}` : '';
|
|
77
|
+
return ` ${displayName(user)}${suffix}${bio}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function searchUsers(query) {
|
|
81
|
+
const res = await api(`/social/search?q=${encodeURIComponent(query)}`);
|
|
82
|
+
if (res.status !== 200) {
|
|
83
|
+
fail(apiErrorMessage(res, `Search failed (${res.status})`));
|
|
84
|
+
}
|
|
85
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Turn a username/email/name/UUID into a single user object (or exit with
|
|
90
|
+
* candidates listed). Never guesses between multiple plausible people.
|
|
91
|
+
*/
|
|
92
|
+
async function resolveUser(who) {
|
|
93
|
+
if (UUID_RE.test(who)) {
|
|
94
|
+
return { id: who };
|
|
95
|
+
}
|
|
96
|
+
const query = who.replace(/^@/, '');
|
|
97
|
+
const results = await searchUsers(query);
|
|
98
|
+
if (results.length === 0) {
|
|
99
|
+
fail(`No one found matching "${who}". Try: atris people <name>`);
|
|
100
|
+
}
|
|
101
|
+
const q = query.toLowerCase();
|
|
102
|
+
const exact = results.filter(
|
|
103
|
+
(u) =>
|
|
104
|
+
(u.username && u.username.toLowerCase() === q) ||
|
|
105
|
+
(u.email && u.email.toLowerCase() === q) ||
|
|
106
|
+
(u.name && u.name.toLowerCase() === q)
|
|
107
|
+
);
|
|
108
|
+
const pool = exact.length > 0 ? exact : results;
|
|
109
|
+
if (pool.length === 1) return pool[0];
|
|
110
|
+
console.error(`"${who}" matches ${pool.length} people:`);
|
|
111
|
+
for (const u of pool.slice(0, 8)) console.error(userLine(u));
|
|
112
|
+
fail('Be more specific (use the @username).');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function overview() {
|
|
116
|
+
const [stats, unread, suggested] = await Promise.all([
|
|
117
|
+
api('/social/stats'),
|
|
118
|
+
api('/dm/unread'),
|
|
119
|
+
api('/social/suggested?limit=5'),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
if (stats.status === 200) {
|
|
123
|
+
const s = stats.data || {};
|
|
124
|
+
console.log(`Followers ${s.followers_count ?? s.followers ?? 0} · Following ${s.following_count ?? s.following ?? 0} · Friends ${s.mutuals_count ?? s.mutuals ?? '-'}`);
|
|
125
|
+
}
|
|
126
|
+
if (unread.status === 200 && unread.data) {
|
|
127
|
+
const n = unread.data.total_unread || 0;
|
|
128
|
+
console.log(n > 0 ? `Unread messages: ${n} → atris inbox` : 'No unread messages');
|
|
129
|
+
}
|
|
130
|
+
const sugg = suggested.status === 200 && Array.isArray(suggested.data) ? suggested.data : [];
|
|
131
|
+
if (sugg.length > 0) {
|
|
132
|
+
console.log('\nPeople you may know:');
|
|
133
|
+
for (const u of sugg) console.log(userLine(u));
|
|
134
|
+
console.log('\nAdd one: atris follow <name>');
|
|
135
|
+
}
|
|
136
|
+
console.log('\nInvite a friend: atris invite');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function people(query) {
|
|
140
|
+
if (!query || isHelpToken(query)) {
|
|
141
|
+
showUsageAndExit('Usage: atris people <name or email>');
|
|
142
|
+
}
|
|
143
|
+
const results = await searchUsers(query);
|
|
144
|
+
if (results.length === 0) {
|
|
145
|
+
console.log(`No one found for "${query}". Invite them: atris invite`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`Found ${results.length}:`);
|
|
149
|
+
for (const u of results) console.log(userLine(u));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function follow(who, remove = false) {
|
|
153
|
+
if (!who || isHelpToken(who)) {
|
|
154
|
+
showUsageAndExit(`Usage: atris ${remove ? 'unfollow' : 'follow'} <who>`);
|
|
155
|
+
}
|
|
156
|
+
const user = await resolveUser(who);
|
|
157
|
+
const res = await api(`/social/follow/${user.id}`, { method: remove ? 'DELETE' : 'POST' });
|
|
158
|
+
if (res.status >= 400) {
|
|
159
|
+
fail(apiErrorMessage(res, `Request failed (${res.status})`));
|
|
160
|
+
}
|
|
161
|
+
const label = displayName(user);
|
|
162
|
+
if (remove) {
|
|
163
|
+
console.log(`Unfollowed ${label}.`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const check = await api(`/social/check/${user.id}`);
|
|
167
|
+
const mutual = check.status === 200 && check.data && check.data.is_mutual;
|
|
168
|
+
console.log(mutual
|
|
169
|
+
? `You and ${label} are now friends (you follow each other).`
|
|
170
|
+
: `Now following ${label}. When they follow back, you're friends.`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function friends(kind) {
|
|
174
|
+
if (isHelpToken(kind)) {
|
|
175
|
+
showUsageAndExit('Usage: atris friends [followers|following]');
|
|
176
|
+
}
|
|
177
|
+
const path =
|
|
178
|
+
kind === 'followers' ? '/social/followers'
|
|
179
|
+
: kind === 'following' ? '/social/following'
|
|
180
|
+
: '/social/mutuals';
|
|
181
|
+
const res = await api(path);
|
|
182
|
+
if (res.status !== 200) fail(apiErrorMessage(res, `Request failed (${res.status})`));
|
|
183
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
184
|
+
const label =
|
|
185
|
+
kind === 'followers' ? 'Followers' : kind === 'following' ? 'Following' : 'Friends';
|
|
186
|
+
if (list.length === 0) {
|
|
187
|
+
console.log(kind
|
|
188
|
+
? `${label}: none yet.`
|
|
189
|
+
: 'No friends yet. Find people: atris people <name> · Invite: atris invite');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log(`${label} (${list.length}):`);
|
|
193
|
+
for (const u of list) console.log(userLine(u));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function getConversationWith(who) {
|
|
197
|
+
const user = await resolveUser(who);
|
|
198
|
+
if (!UUID_RE.test(user.id)) fail('Could not resolve that user.');
|
|
199
|
+
const res = await api('/dm/conversations', {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
body: { user_id: user.id },
|
|
202
|
+
});
|
|
203
|
+
if (res.status >= 400) {
|
|
204
|
+
fail(apiErrorMessage(res, `Could not open conversation (${res.status})`));
|
|
205
|
+
}
|
|
206
|
+
return { conversation: res.data, user };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function message(who, text) {
|
|
210
|
+
if (!who || isHelpToken(who) || isHelpToken(text)) {
|
|
211
|
+
showUsageAndExit('Usage: atris msg <who> [message]');
|
|
212
|
+
}
|
|
213
|
+
const { conversation } = await getConversationWith(who);
|
|
214
|
+
const other = displayName(conversation.other_user || {});
|
|
215
|
+
|
|
216
|
+
if (!text) {
|
|
217
|
+
const msgs = await api(`/dm/conversations/${conversation.id}/messages?limit=30`);
|
|
218
|
+
if (msgs.status !== 200) fail(apiErrorMessage(msgs, 'Could not load messages'));
|
|
219
|
+
const list = (msgs.data && msgs.data.messages) || [];
|
|
220
|
+
if (list.length === 0) {
|
|
221
|
+
console.log(`No messages with ${other} yet. Send one: atris msg ${who} "hello"`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`Conversation with ${other}:\n`);
|
|
225
|
+
for (const m of list) {
|
|
226
|
+
const when = new Date(m.created_at).toLocaleString();
|
|
227
|
+
console.log(` ${m.is_mine ? 'you' : other} · ${when}`);
|
|
228
|
+
console.log(` ${m.content}\n`);
|
|
229
|
+
}
|
|
230
|
+
await api(`/dm/conversations/${conversation.id}/read`, { method: 'POST' });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const res = await api(`/dm/conversations/${conversation.id}/messages`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: { content: text, content_type: 'text' },
|
|
237
|
+
});
|
|
238
|
+
if (res.status >= 400) fail(apiErrorMessage(res, `Send failed (${res.status})`));
|
|
239
|
+
console.log(`Sent to ${other}.`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function inbox() {
|
|
243
|
+
const res = await api('/dm/conversations');
|
|
244
|
+
if (res.status !== 200) fail(apiErrorMessage(res, `Request failed (${res.status})`));
|
|
245
|
+
const convs = (res.data && res.data.conversations) || [];
|
|
246
|
+
if (convs.length === 0) {
|
|
247
|
+
console.log('No conversations yet. Start one: atris msg <who> "hello"');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
convs.sort((a, b) => (b.unread_count || 0) - (a.unread_count || 0));
|
|
251
|
+
console.log(`Conversations (${convs.length}):\n`);
|
|
252
|
+
for (const c of convs) {
|
|
253
|
+
const other = displayName(c.other_user || {});
|
|
254
|
+
const unread = c.unread_count > 0 ? ` · ${c.unread_count} unread` : '';
|
|
255
|
+
const preview = c.last_message_preview ? `\n ${c.last_message_preview.slice(0, 80)}` : '';
|
|
256
|
+
console.log(` ${other}${unread}${preview}`);
|
|
257
|
+
}
|
|
258
|
+
console.log('\nRead one: atris msg <who>');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseInviteFlags(args) {
|
|
262
|
+
const flags = {};
|
|
263
|
+
const rest = [];
|
|
264
|
+
const known = {
|
|
265
|
+
'--for': 'name', '--email': 'email', '--company': 'company', '--business': 'company',
|
|
266
|
+
'--role': 'role', '--why': 'why', '--use': 'use', '--agent': 'agent', '--pack': 'pack',
|
|
267
|
+
'--days': 'days',
|
|
268
|
+
};
|
|
269
|
+
for (let i = 0; i < args.length; i++) {
|
|
270
|
+
const key = known[args[i]];
|
|
271
|
+
if (key) {
|
|
272
|
+
flags[key] = args[i + 1] || '';
|
|
273
|
+
i++;
|
|
274
|
+
} else if (args[i] === '--any-email') {
|
|
275
|
+
flags.anyEmail = true;
|
|
276
|
+
} else {
|
|
277
|
+
rest.push(args[i]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { flags, rest };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function pickSourceAgent(nameFilter) {
|
|
284
|
+
const res = await api('/agent/my-agents');
|
|
285
|
+
const agents = (res.data && res.data.my_agents) || [];
|
|
286
|
+
if (agents.length === 0) {
|
|
287
|
+
fail('You have no agents to clone for a personalized invite. Create one first, or drop the personal flags for a plain link.');
|
|
288
|
+
}
|
|
289
|
+
if (nameFilter) {
|
|
290
|
+
const q = nameFilter.toLowerCase();
|
|
291
|
+
const match = agents.find((a) => (a.name || '').toLowerCase() === q)
|
|
292
|
+
|| agents.find((a) => (a.name || '').toLowerCase().includes(q))
|
|
293
|
+
|| (UUID_RE.test(nameFilter) ? { id: nameFilter, name: nameFilter } : null);
|
|
294
|
+
if (!match) {
|
|
295
|
+
console.error(`No agent matching "${nameFilter}". Your agents:`);
|
|
296
|
+
for (const a of agents) console.error(` ${a.name}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
return match;
|
|
300
|
+
}
|
|
301
|
+
return agents[0];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function inviteWizard() {
|
|
305
|
+
const { promptUser } = require('../utils/auth');
|
|
306
|
+
console.log('Personalized invite. Enter to skip any question.\n');
|
|
307
|
+
const flags = {};
|
|
308
|
+
flags.name = (await promptUser('Who is it for? (name): ')).trim();
|
|
309
|
+
if (!flags.name) {
|
|
310
|
+
console.log('\nNo name given, making a plain shareable link instead.');
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
flags.email = (await promptUser('Their email (locks the invite to them): ')).trim();
|
|
314
|
+
flags.company = (await promptUser('Their business (name or kind, e.g. "Rivera HVAC"): ')).trim();
|
|
315
|
+
flags.use = (await promptUser('What should their agent help with?: ')).trim();
|
|
316
|
+
flags.why = (await promptUser('Why are you reaching out? (private context): ')).trim();
|
|
317
|
+
const pack = (await promptUser('Pack to include [atris]: ')).trim();
|
|
318
|
+
flags.pack = pack || 'atris';
|
|
319
|
+
console.log('');
|
|
320
|
+
return flags;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function invite(argv) {
|
|
324
|
+
const argvList = Array.isArray(argv) ? argv : [];
|
|
325
|
+
if (argsWantHelp(argvList)) {
|
|
326
|
+
printHelp();
|
|
327
|
+
process.exit(2);
|
|
328
|
+
}
|
|
329
|
+
let { flags } = parseInviteFlags(argvList);
|
|
330
|
+
if (Object.keys(flags).length === 0 && process.stdout.isTTY && process.stdin.isTTY) {
|
|
331
|
+
flags = await inviteWizard();
|
|
332
|
+
}
|
|
333
|
+
const personal = flags.name || flags.email || flags.company || flags.why || flags.use;
|
|
334
|
+
|
|
335
|
+
// Plain link (optionally carrying a pack the claimer's CLI auto-installs).
|
|
336
|
+
if (!personal) {
|
|
337
|
+
const body = { resource_type: 'atris_plain' };
|
|
338
|
+
if (flags.pack) {
|
|
339
|
+
body.pre_config = { metadata: { pack_slug: flags.pack } };
|
|
340
|
+
}
|
|
341
|
+
const res = await api('/invites', { method: 'POST', body });
|
|
342
|
+
if (res.status >= 400) fail(apiErrorMessage(res, `Could not create invite (${res.status})`));
|
|
343
|
+
const data = res.data || {};
|
|
344
|
+
const url = data.share_url || `${getAppBaseUrl()}/invite/${data.invite_code}`;
|
|
345
|
+
console.log('Invite link (share it with anyone):\n');
|
|
346
|
+
console.log(` ${url}\n`);
|
|
347
|
+
if (flags.pack) {
|
|
348
|
+
console.log(`Comes with the "${flags.pack}" pack. In a terminal they run: atris join ${data.invite_code}`);
|
|
349
|
+
} else {
|
|
350
|
+
console.log('When they sign up through it, find them with: atris people <their name>');
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Personalized invite: clones one of your agents for them, writes the letter
|
|
356
|
+
// and an email draft, optionally locks the claim to their email.
|
|
357
|
+
const agent = await pickSourceAgent(flags.agent);
|
|
358
|
+
const body = {
|
|
359
|
+
source_agent_id: agent.id,
|
|
360
|
+
target_name: flags.name || null,
|
|
361
|
+
target_email: flags.email || null,
|
|
362
|
+
target_company: flags.company || null,
|
|
363
|
+
target_role: flags.role || null,
|
|
364
|
+
target_context: flags.why || null,
|
|
365
|
+
use_case: flags.use || null,
|
|
366
|
+
restrict_to_email: !flags.anyEmail,
|
|
367
|
+
expires_days: flags.days ? Math.max(1, Math.min(90, parseInt(flags.days, 10) || 14)) : 14,
|
|
368
|
+
};
|
|
369
|
+
if (flags.pack) body.pack_slug = flags.pack;
|
|
370
|
+
|
|
371
|
+
console.log(`Building a personalized invite (cloning "${agent.name}")...`);
|
|
372
|
+
const res = await api('/invites/personalize', { method: 'POST', body, timeoutMs: 90000 });
|
|
373
|
+
if (res.status === 402) fail('Personalized invites need a paid plan.');
|
|
374
|
+
if (res.status >= 400) fail(apiErrorMessage(res, `Could not create invite (${res.status})`));
|
|
375
|
+
const data = res.data || {};
|
|
376
|
+
|
|
377
|
+
console.log(`\nInvite for ${flags.name || flags.email}${flags.pack ? ` · pack: ${flags.pack}` : ''}`);
|
|
378
|
+
console.log(`\n ${data.share_url}\n`);
|
|
379
|
+
if (data.email_subject || data.email_body) {
|
|
380
|
+
console.log('Email draft:\n');
|
|
381
|
+
if (data.email_subject) console.log(` Subject: ${data.email_subject}\n`);
|
|
382
|
+
for (const line of String(data.email_body || '').split('\n')) console.log(` ${line}`);
|
|
383
|
+
console.log('');
|
|
384
|
+
}
|
|
385
|
+
if (flags.email && !flags.anyEmail) {
|
|
386
|
+
console.log(`Locked to ${flags.email}. They must sign up with that address.`);
|
|
387
|
+
}
|
|
388
|
+
console.log(`Terminal path for them: atris join ${data.invite_code}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function join(code, rest = []) {
|
|
392
|
+
if (isHelpToken(code) || argsWantHelp(rest)) {
|
|
393
|
+
showUsageAndExit('Usage: atris join <invite-code or invite link> [--handle <yourhandle>]');
|
|
394
|
+
}
|
|
395
|
+
if (!code) fail('Usage: atris join <invite-code or invite link> [--handle <yourhandle>]');
|
|
396
|
+
const cleaned = String(code).trim().split('/').filter(Boolean).pop().toUpperCase();
|
|
397
|
+
|
|
398
|
+
// Public preview first, so this works before login.
|
|
399
|
+
const { apiRequestJson } = require('../utils/api');
|
|
400
|
+
const preview = await apiRequestJson(`/invites/${encodeURIComponent(cleaned)}`);
|
|
401
|
+
if (preview.status === 404) fail('That invite does not exist or has expired.');
|
|
402
|
+
if (preview.status >= 400) fail(apiErrorMessage(preview, `Could not load invite (${preview.status})`));
|
|
403
|
+
const inv = preview.data || {};
|
|
404
|
+
|
|
405
|
+
const from = inv.inviter_display_name || 'someone on Atris';
|
|
406
|
+
console.log(`Invite from ${from}${inv.resource_name ? ` · ${inv.resource_name}` : ''}`);
|
|
407
|
+
if (inv.preview && inv.preview.letter) {
|
|
408
|
+
console.log('');
|
|
409
|
+
const shareUrl = `${getAppBaseUrl()}/invite/${cleaned}`;
|
|
410
|
+
const letter = String(inv.preview.letter).replace(/\{invite_url\}/g, shareUrl);
|
|
411
|
+
for (const line of letter.split('\n')) console.log(` ${line}`);
|
|
412
|
+
}
|
|
413
|
+
console.log('');
|
|
414
|
+
if (!inv.can_claim) fail('This invite has no claims left.');
|
|
415
|
+
|
|
416
|
+
// No account yet? Create one right here — signup is browser-free
|
|
417
|
+
// (commands/signup.js), so a headless terminal can claim in one run.
|
|
418
|
+
const creds = loadCredentials();
|
|
419
|
+
if (!creds || !creds.token) {
|
|
420
|
+
let handle = null;
|
|
421
|
+
for (let i = 0; i < rest.length; i++) {
|
|
422
|
+
if (rest[i] === '--handle' && rest[i + 1]) handle = String(rest[i + 1]).trim().toLowerCase();
|
|
423
|
+
}
|
|
424
|
+
if (!handle && process.stdout.isTTY && process.stdin.isTTY) {
|
|
425
|
+
const { promptUser } = require('../utils/auth');
|
|
426
|
+
console.log('You need an Atris account to accept it. Let\'s make one now, no browser needed.');
|
|
427
|
+
handle = (await promptUser('Pick a handle (3-30 chars, a-z and 0-9): ')).trim().toLowerCase();
|
|
428
|
+
}
|
|
429
|
+
if (!handle) {
|
|
430
|
+
console.log('You need an Atris account to accept it. No browser needed:');
|
|
431
|
+
console.log(` atris join ${cleaned} --handle <yourhandle>`);
|
|
432
|
+
console.log(`Or sign up in a browser: ${getAppBaseUrl()}/invite/${cleaned}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const { signupCommand } = require('./signup');
|
|
436
|
+
const signupExit = await signupCommand([handle]);
|
|
437
|
+
if (signupExit !== 0) fail('Account creation failed, so the invite was not claimed. Fix the handle and rerun.');
|
|
438
|
+
console.log('');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const { randomUUID } = require('crypto');
|
|
442
|
+
const res = await api(`/invites/${encodeURIComponent(cleaned)}/claim`, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
body: { metadata: { source: 'cli' } },
|
|
445
|
+
headers: { 'Idempotency-Key': randomUUID() },
|
|
446
|
+
});
|
|
447
|
+
if (res.status >= 400) fail(apiErrorMessage(res, `Could not claim invite (${res.status})`));
|
|
448
|
+
const claim = res.data || {};
|
|
449
|
+
if (claim.status === 'claimed' || claim.status === 'already_claimed' || claim.status === 'already_member') {
|
|
450
|
+
console.log(claim.status === 'claimed' ? `You're in. Connected through ${from}'s invite.` : 'Already claimed. You are in.');
|
|
451
|
+
} else {
|
|
452
|
+
fail(`Invite could not be claimed (${claim.status}).`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// A business on the invite gets its own local folder; the pack lands inside
|
|
456
|
+
// it. That folder is the workspace, and one command later takes it cloud.
|
|
457
|
+
let workDir = null;
|
|
458
|
+
if (inv.business) {
|
|
459
|
+
const fs = require('fs');
|
|
460
|
+
const path = require('path');
|
|
461
|
+
const slug = String(inv.business).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'business';
|
|
462
|
+
workDir = path.resolve(process.cwd(), slug);
|
|
463
|
+
if (!fs.existsSync(workDir)) fs.mkdirSync(workDir, { recursive: true });
|
|
464
|
+
console.log(`\nBusiness workspace: ${slug}/`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (inv.pack) {
|
|
468
|
+
console.log(`\nThis invite comes with the "${inv.pack}" pack. Installing...`);
|
|
469
|
+
const { spawnSync } = require('child_process');
|
|
470
|
+
const packArgs = [require('path').join(__dirname, '..', 'bin', 'atris.js'), 'pack', 'install', inv.pack];
|
|
471
|
+
const result = spawnSync(process.execPath, packArgs, {
|
|
472
|
+
stdio: 'inherit',
|
|
473
|
+
cwd: workDir || process.cwd(),
|
|
474
|
+
});
|
|
475
|
+
if (result.status !== 0) {
|
|
476
|
+
console.log(`Install did not finish. Retry with: atris pack install ${inv.pack}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log('\nNext:');
|
|
481
|
+
if (workDir) {
|
|
482
|
+
const rel = require('path').basename(workDir);
|
|
483
|
+
console.log(` cd ${rel} your business workspace`);
|
|
484
|
+
console.log(` atris business init "${inv.business}" take this folder to the cloud when ready`);
|
|
485
|
+
}
|
|
486
|
+
console.log(' atris social your overview');
|
|
487
|
+
console.log(' atris people <name> find your inviter and follow back');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function printHelp() {
|
|
491
|
+
console.log(`atris social — people, friends, and messages on Atris
|
|
492
|
+
|
|
493
|
+
atris social Your overview
|
|
494
|
+
atris people <query> Find people
|
|
495
|
+
atris follow <who> Add a friend (follow)
|
|
496
|
+
atris unfollow <who> Unfollow
|
|
497
|
+
atris friends Your friends (mutual follows)
|
|
498
|
+
atris friends followers Who follows you
|
|
499
|
+
atris friends following Who you follow
|
|
500
|
+
atris msg <who> <text> Send a message
|
|
501
|
+
atris msg <who> Read the conversation
|
|
502
|
+
atris inbox All conversations
|
|
503
|
+
atris invite Guided personalized invite (just answer questions)
|
|
504
|
+
atris invite --pack <slug> Plain link that also installs a pack on claim
|
|
505
|
+
atris invite --for "Name" --email a@b.com --business "Their Co" --use "..." [--why "..."] [--agent <name>] [--pack <slug>]
|
|
506
|
+
Personalized invite: clones your agent for them,
|
|
507
|
+
writes the letter + email draft, locks to their email
|
|
508
|
+
atris join <code|link> Accept an invite; installs the pack and, for
|
|
509
|
+
business invites, scaffolds the business folder`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function socialCommand(argv) {
|
|
513
|
+
const args = argv || process.argv.slice(3);
|
|
514
|
+
const sub = args[0];
|
|
515
|
+
switch (sub) {
|
|
516
|
+
case undefined:
|
|
517
|
+
return overview();
|
|
518
|
+
case 'help':
|
|
519
|
+
case '--help':
|
|
520
|
+
case '-h':
|
|
521
|
+
return printHelp();
|
|
522
|
+
case 'find':
|
|
523
|
+
case 'people':
|
|
524
|
+
case 'search':
|
|
525
|
+
return people(args.slice(1).join(' '));
|
|
526
|
+
case 'add':
|
|
527
|
+
case 'follow':
|
|
528
|
+
return follow(args.slice(1).join(' '));
|
|
529
|
+
case 'remove':
|
|
530
|
+
case 'unfollow':
|
|
531
|
+
return follow(args.slice(1).join(' '), true);
|
|
532
|
+
case 'friends':
|
|
533
|
+
if (argsWantHelp(args.slice(1))) showUsageAndExit('Usage: atris friends [followers|following]');
|
|
534
|
+
return friends(args[1]);
|
|
535
|
+
case 'followers':
|
|
536
|
+
case 'following':
|
|
537
|
+
return friends(sub);
|
|
538
|
+
case 'msg':
|
|
539
|
+
case 'message':
|
|
540
|
+
case 'dm':
|
|
541
|
+
return message(args[1], args.slice(2).join(' '));
|
|
542
|
+
case 'inbox':
|
|
543
|
+
if (argsWantHelp(args.slice(1))) showUsageAndExit('Usage: atris inbox');
|
|
544
|
+
return inbox();
|
|
545
|
+
case 'invite':
|
|
546
|
+
return invite(args.slice(1));
|
|
547
|
+
case 'join':
|
|
548
|
+
return join(args[1], args.slice(2));
|
|
549
|
+
default:
|
|
550
|
+
printHelp();
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Top-level aliases: `atris people x`, `atris follow x`, `atris msg x hi`, ...
|
|
556
|
+
async function peopleCommand() {
|
|
557
|
+
const args = process.argv.slice(3);
|
|
558
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris people <name or email>');
|
|
559
|
+
return people(args.join(' '));
|
|
560
|
+
}
|
|
561
|
+
async function followCommand() {
|
|
562
|
+
const args = process.argv.slice(3);
|
|
563
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris follow <who>');
|
|
564
|
+
return follow(args.join(' '));
|
|
565
|
+
}
|
|
566
|
+
async function unfollowCommand() {
|
|
567
|
+
const args = process.argv.slice(3);
|
|
568
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris unfollow <who>');
|
|
569
|
+
return follow(args.join(' '), true);
|
|
570
|
+
}
|
|
571
|
+
async function friendsCommand() {
|
|
572
|
+
const args = process.argv.slice(3);
|
|
573
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris friends [followers|following]');
|
|
574
|
+
return friends(args[0]);
|
|
575
|
+
}
|
|
576
|
+
async function msgCommand() {
|
|
577
|
+
const args = process.argv.slice(3);
|
|
578
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris msg <who> [message]');
|
|
579
|
+
return message(args[0], args.slice(1).join(' '));
|
|
580
|
+
}
|
|
581
|
+
async function inboxCommand() {
|
|
582
|
+
const args = process.argv.slice(3);
|
|
583
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris inbox');
|
|
584
|
+
return inbox();
|
|
585
|
+
}
|
|
586
|
+
async function inviteCommand() { return invite(process.argv.slice(3)); }
|
|
587
|
+
async function joinCommand() {
|
|
588
|
+
const args = process.argv.slice(3);
|
|
589
|
+
if (argsWantHelp(args)) showUsageAndExit('Usage: atris join <invite-code or invite link> [--handle <yourhandle>]');
|
|
590
|
+
return join(args[0], args.slice(1));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
module.exports = {
|
|
594
|
+
socialCommand,
|
|
595
|
+
peopleCommand,
|
|
596
|
+
followCommand,
|
|
597
|
+
unfollowCommand,
|
|
598
|
+
friendsCommand,
|
|
599
|
+
msgCommand,
|
|
600
|
+
inboxCommand,
|
|
601
|
+
inviteCommand,
|
|
602
|
+
joinCommand,
|
|
603
|
+
};
|