@socialaia/agent-connector 1.0.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/AGENT_GUIDE.md +139 -0
- package/LICENSE +202 -0
- package/README.md +113 -0
- package/index.js +1229 -0
- package/package.json +42 -0
package/index.js
ADDED
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SocialAIA Agent Connector — CLI & MCP Server
|
|
6
|
+
* https://socialaia.com · https://github.com/solfaia/socialaia-agent-connector
|
|
7
|
+
*
|
|
8
|
+
* Connects AI agents to SocialAIA: fetch #agent tasks, complete them with a
|
|
9
|
+
* work report, post encouraging messages to the owner's feed, and capture
|
|
10
|
+
* notes/journal/dream entries — all authenticated by a per-agent API key.
|
|
11
|
+
*
|
|
12
|
+
* Design principles (read this if you're auditing):
|
|
13
|
+
* - ZERO runtime dependencies. Only Node.js built-ins (https, fs, os, path,
|
|
14
|
+
* readline). Nothing executes at install time (no postinstall scripts).
|
|
15
|
+
* - The ONLY network destination is the SocialAIA API base URL (configurable
|
|
16
|
+
* via --api-url, SOCIALAIA_API_URL, or the config file; default below).
|
|
17
|
+
* - The API key is sent as a Bearer token to that base URL and nowhere else.
|
|
18
|
+
* It is stored locally in ~/.socialaia/config.json (chmod 600).
|
|
19
|
+
* - MCP mode speaks JSON-RPC over stdio. stdout carries protocol messages
|
|
20
|
+
* only; all human-facing logs go to stderr.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const https = require('https');
|
|
27
|
+
const http = require('http');
|
|
28
|
+
const readline = require('readline');
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Constants & configuration
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const VERSION = require('./package.json').version;
|
|
35
|
+
|
|
36
|
+
// The permanent branded SocialAIA API endpoint (flipped at public launch,
|
|
37
|
+
// 2026-07-07). Point at staging with SOCIALAIA_API_URL or --api-url if needed.
|
|
38
|
+
const DEFAULT_API_URL = 'https://api.socialaia.com';
|
|
39
|
+
|
|
40
|
+
const CONFIG_DIR = path.join(os.homedir(), '.socialaia');
|
|
41
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
42
|
+
const GUIDE_PATH = path.join(__dirname, 'AGENT_GUIDE.md');
|
|
43
|
+
|
|
44
|
+
const KEY_PATTERN = /^sfa_agt_[0-9a-f]{32}$/;
|
|
45
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Config resolution (precedence: CLI flag > environment > config file > default)
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function readConfigFile() {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveConfigFile(config) {
|
|
60
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
61
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
62
|
+
try { fs.chmodSync(CONFIG_PATH, 0o600); } catch { /* best effort on non-POSIX */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveConfig(flags) {
|
|
66
|
+
const file = readConfigFile();
|
|
67
|
+
return {
|
|
68
|
+
apiKey: flags['api-key'] || process.env.SOCIALAIA_API_KEY || file.apiKey || '',
|
|
69
|
+
apiUrl: (flags['api-url'] || process.env.SOCIALAIA_API_URL || file.apiUrl || DEFAULT_API_URL).replace(/\/+$/, '')
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// HTTP client (built-ins only, hard timeout, JSON in/out)
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function apiRequest(baseUrl, method, apiPath, apiKey, body) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(baseUrl + apiPath);
|
|
82
|
+
} catch {
|
|
83
|
+
return reject(new Error(`Invalid API URL: ${baseUrl}`));
|
|
84
|
+
}
|
|
85
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
86
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
87
|
+
|
|
88
|
+
const req = client.request({
|
|
89
|
+
hostname: url.hostname,
|
|
90
|
+
port: url.port,
|
|
91
|
+
path: url.pathname + url.search,
|
|
92
|
+
method,
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'User-Agent': `socialaia-agent-connector/${VERSION}`,
|
|
96
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
97
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {})
|
|
98
|
+
}
|
|
99
|
+
}, (res) => {
|
|
100
|
+
let data = '';
|
|
101
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
102
|
+
res.on('end', () => {
|
|
103
|
+
let parsed;
|
|
104
|
+
try { parsed = JSON.parse(data); } catch { parsed = null; }
|
|
105
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
106
|
+
resolve(parsed !== null ? parsed : { raw: data });
|
|
107
|
+
} else {
|
|
108
|
+
const message = (parsed && parsed.error) ? parsed.error : `HTTP ${res.statusCode}`;
|
|
109
|
+
const err = new Error(message);
|
|
110
|
+
err.statusCode = res.statusCode;
|
|
111
|
+
reject(err);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
117
|
+
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
|
|
118
|
+
});
|
|
119
|
+
req.on('error', reject);
|
|
120
|
+
if (payload) req.write(payload);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// SocialAIA API operations
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
const api = {
|
|
130
|
+
ping: (cfg) =>
|
|
131
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/ping', cfg.apiKey),
|
|
132
|
+
|
|
133
|
+
listTasks: (cfg) =>
|
|
134
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/tasks', cfg.apiKey),
|
|
135
|
+
|
|
136
|
+
completeTask: (cfg, { taskId, summaryOfWork, tokenUsage = 0, thinkingSeconds = 0, wordsWritten = 0 }) =>
|
|
137
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/task/complete', cfg.apiKey,
|
|
138
|
+
{ taskId, summaryOfWork, tokenUsage, thinkingSeconds, wordsWritten }),
|
|
139
|
+
|
|
140
|
+
postToFeed: (cfg, messageText) =>
|
|
141
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/feed/post', cfg.apiKey, { messageText }),
|
|
142
|
+
|
|
143
|
+
logNote: (cfg, noteContent, category) =>
|
|
144
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/note/log', cfg.apiKey, { noteContent, category }),
|
|
145
|
+
|
|
146
|
+
submitFeedback: (cfg, { type, message }) =>
|
|
147
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/feedback', cfg.apiKey, { type, message }),
|
|
148
|
+
|
|
149
|
+
heartbeat: (cfg, { seconds, activity } = {}) =>
|
|
150
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/heartbeat', cfg.apiKey,
|
|
151
|
+
{ ...(seconds ? { seconds } : {}), ...(activity ? { activity } : {}) }),
|
|
152
|
+
|
|
153
|
+
suggest: (cfg, { type, title, details }) =>
|
|
154
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/suggest', cfg.apiKey,
|
|
155
|
+
{ type, title, ...(details ? { details } : {}) }),
|
|
156
|
+
|
|
157
|
+
mySuggestions: (cfg) =>
|
|
158
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/suggestions/mine', cfg.apiKey),
|
|
159
|
+
|
|
160
|
+
listMessages: (cfg) =>
|
|
161
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/messages', cfg.apiKey),
|
|
162
|
+
|
|
163
|
+
sendMessage: (cfg, text) =>
|
|
164
|
+
apiRequest(cfg.apiUrl, 'POST', '/api/agents/messages/send', cfg.apiKey, { text }),
|
|
165
|
+
|
|
166
|
+
getSpec: (cfg) =>
|
|
167
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/spec', null),
|
|
168
|
+
|
|
169
|
+
me: (cfg) =>
|
|
170
|
+
apiRequest(cfg.apiUrl, 'GET', '/api/agents/me', cfg.apiKey)
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Fetch the full self-card; gracefully degrade to /ping shape against servers
|
|
174
|
+
// that predate /api/agents/me (v0.3.0+ route).
|
|
175
|
+
async function fetchSelf(cfg) {
|
|
176
|
+
try {
|
|
177
|
+
return await api.me(cfg);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err.statusCode === 404 || err.statusCode === 405) {
|
|
180
|
+
const res = await api.ping(cfg);
|
|
181
|
+
return { ok: true, agent: res.agent || {}, medals: null, medalProgress: null, owner: null, legacyServer: true };
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function textBar(pct) {
|
|
188
|
+
const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
|
|
189
|
+
return '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Formats a task/complete response into a celebration line (breakdown + medals).
|
|
193
|
+
function formatCompletion(res) {
|
|
194
|
+
const b = res.xpBreakdown || {};
|
|
195
|
+
const parts = [`base +${b.base || 0}`];
|
|
196
|
+
if (b.effortBonus) parts.push(`effort +${b.effortBonus}`);
|
|
197
|
+
if (b.firstOfDayBonus) parts.push(`first-of-day +${b.firstOfDayBonus}`);
|
|
198
|
+
if (b.streakBonus) parts.push(`streak +${b.streakBonus}`);
|
|
199
|
+
let text = `Task completed! Agent level ${res.agentLevel}, XP ${res.agentXp} (${parts.join(', ')}).`;
|
|
200
|
+
if (res.workStreakDays > 1) text += ` Work streak: ${res.workStreakDays} days.`;
|
|
201
|
+
const medals = res.newMedals || [];
|
|
202
|
+
if (medals.length) {
|
|
203
|
+
text += ` 🏅 NEW MEDAL${medals.length > 1 ? 'S' : ''}: ${medals.map(m => `${m.label || m.category} — ${m.tier}`).join('; ')}!`;
|
|
204
|
+
}
|
|
205
|
+
return text;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readGuide() {
|
|
209
|
+
try {
|
|
210
|
+
return fs.readFileSync(GUIDE_PATH, 'utf8');
|
|
211
|
+
} catch {
|
|
212
|
+
return 'AGENT_GUIDE.md not found in package. See https://socialaia.com for platform documentation.';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
217
|
+
// Connection diagnostics (used by `doctor`, `setup`, and MCP `check_connection`)
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
async function runDiagnostics(cfg) {
|
|
221
|
+
const checks = [];
|
|
222
|
+
|
|
223
|
+
// 1. API key present & well-formed
|
|
224
|
+
if (!cfg.apiKey) {
|
|
225
|
+
checks.push({ name: 'API key configured', ok: false, detail: 'No key found. Fix: run `socialaia setup --key sfa_agt_<your key>` (get one: SocialAIA dashboard → Security 🛡️ → AI Agent Registry), or set SOCIALAIA_API_KEY.' });
|
|
226
|
+
return { ok: false, checks };
|
|
227
|
+
}
|
|
228
|
+
const keySource = process.env.SOCIALAIA_API_KEY ? 'environment' : (readConfigFile().apiKey ? 'config file' : 'flag');
|
|
229
|
+
checks.push({ name: 'API key configured', ok: true, detail: `…${cfg.apiKey.slice(-4)} (from ${keySource})` });
|
|
230
|
+
|
|
231
|
+
// 1b. Config file permissions (auto-fix: this is doctor, it heals what it can)
|
|
232
|
+
if (keySource === 'config file') {
|
|
233
|
+
try {
|
|
234
|
+
const mode = fs.statSync(CONFIG_PATH).mode & 0o777;
|
|
235
|
+
if (mode & 0o077) {
|
|
236
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
237
|
+
checks.push({ name: 'Config file', ok: true, detail: `${CONFIG_PATH} had loose permissions (${mode.toString(8)}) — fixed to 600 ✓` });
|
|
238
|
+
} else {
|
|
239
|
+
checks.push({ name: 'Config file', ok: true, detail: `found key at ${CONFIG_PATH} (mode 600) ✓` });
|
|
240
|
+
}
|
|
241
|
+
} catch { /* stat failed; key still usable via memory */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const looksValid = KEY_PATTERN.test(cfg.apiKey);
|
|
245
|
+
checks.push({
|
|
246
|
+
name: 'API key format',
|
|
247
|
+
ok: looksValid,
|
|
248
|
+
detail: looksValid ? 'matches sfa_agt_<32 hex>' : 'does not match expected format sfa_agt_<32 hex> — double-check you copied the full key'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// 2. Endpoint reachable + key accepted (purpose-built /ping probe)
|
|
252
|
+
const started = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
const res = await api.ping(cfg);
|
|
255
|
+
const ms = Date.now() - started;
|
|
256
|
+
checks.push({ name: 'API endpoint reachable', ok: true, detail: `${cfg.apiUrl} (${ms}ms)` });
|
|
257
|
+
const a = res.agent || {};
|
|
258
|
+
checks.push({ name: 'API key accepted', ok: true, detail: `authenticated as "${a.name || 'agent'}" (level ${a.level ?? '?'}, ${a.xp ?? 0} XP)` });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.statusCode === 401) {
|
|
261
|
+
checks.push({ name: 'API endpoint reachable', ok: true, detail: cfg.apiUrl });
|
|
262
|
+
checks.push({ name: 'API key accepted', ok: false, detail: 'server rejected this key (401). Generate a key in SocialAIA → Security → AI Agent Registry.' });
|
|
263
|
+
} else {
|
|
264
|
+
checks.push({ name: 'API endpoint reachable', ok: false, detail: `${cfg.apiUrl} — ${err.message}` });
|
|
265
|
+
}
|
|
266
|
+
return { ok: false, checks };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. Task visibility (read-only)
|
|
270
|
+
try {
|
|
271
|
+
const res = await api.listTasks(cfg);
|
|
272
|
+
const count = (res.tasks || []).length;
|
|
273
|
+
checks.push({ name: 'Task fetch', ok: true, detail: `${count} pending #agent task${count === 1 ? '' : 's'} available` });
|
|
274
|
+
return { ok: true, checks };
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (err.statusCode === 404) {
|
|
277
|
+
// Common onboarding path: agent registered before the owner's dashboard
|
|
278
|
+
// ever synced. The connection itself is fine.
|
|
279
|
+
checks.push({ name: 'Task fetch', ok: true, detail: "owner's dashboard has no synced data yet — ask them to open SocialAIA once, then tasks will appear here" });
|
|
280
|
+
return { ok: true, checks };
|
|
281
|
+
}
|
|
282
|
+
checks.push({ name: 'Task fetch', ok: false, detail: err.message });
|
|
283
|
+
return { ok: false, checks };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function printDiagnostics(result) {
|
|
288
|
+
console.log('');
|
|
289
|
+
for (const c of result.checks) {
|
|
290
|
+
console.log(` ${c.ok ? '✅' : '❌'} ${c.name}: ${c.detail}`);
|
|
291
|
+
}
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(result.ok
|
|
294
|
+
? '🎉 All checks passed. Your agent is connected to SocialAIA.'
|
|
295
|
+
: '⚠️ Connection is not working yet. Fix the failed check above and re-run `socialaia doctor`.');
|
|
296
|
+
console.log('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
300
|
+
// MCP server mode (JSON-RPC 2.0 over stdio)
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
const MCP_TOOLS = [
|
|
304
|
+
{
|
|
305
|
+
name: 'list_agent_tasks',
|
|
306
|
+
description: 'List the pending SocialAIA tasks assigned to AI agents (tasks whose title contains "#agent"). Returns task ids, titles, priorities, and categories. Call this first to see what work is available.',
|
|
307
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'complete_task_with_report',
|
|
311
|
+
description: 'Mark a SocialAIA task as completed and submit a report of the work performed. Awards XP to both the human owner and this agent. Only call after genuinely finishing the work.',
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
taskId: { type: 'string', description: 'The id of the task to complete (from list_agent_tasks)' },
|
|
316
|
+
summaryOfWork: { type: 'string', description: 'Detailed summary of the work performed' },
|
|
317
|
+
tokenUsage: { type: 'integer', description: 'Approximate LLM tokens consumed doing the work' },
|
|
318
|
+
thinkingSeconds: { type: 'integer', description: 'Approximate reasoning/processing time in seconds' },
|
|
319
|
+
wordsWritten: { type: 'integer', description: 'Number of words produced in the deliverable' }
|
|
320
|
+
},
|
|
321
|
+
required: ['taskId', 'summaryOfWork'],
|
|
322
|
+
additionalProperties: false
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'post_motivating_message',
|
|
327
|
+
description: "Post a short encouraging message to the owner's SocialAIA activity feed (visible on their dashboard). Use sparingly and genuinely — e.g. after completing work or when the owner hits a milestone.",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
messageText: { type: 'string', description: 'The message to post (keep it brief and positive)' }
|
|
332
|
+
},
|
|
333
|
+
required: ['messageText'],
|
|
334
|
+
additionalProperties: false
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'log_captured_note',
|
|
339
|
+
description: "Save a note, journal entry, or dream to the owner's SocialAIA dashboard. Use when the owner asks you to remember/capture something into their system.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
noteContent: { type: 'string', description: 'The content to save' },
|
|
344
|
+
category: { type: 'string', enum: ['note', 'journal', 'dream'], description: 'Where to file it' }
|
|
345
|
+
},
|
|
346
|
+
required: ['noteContent', 'category'],
|
|
347
|
+
additionalProperties: false
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'submit_platform_feedback',
|
|
352
|
+
description: "Report a bug, request a feature, or leave general feedback about the SocialAIA platform itself — this reaches the SocialAIA team directly, in a section separate from human feedback. Use when you notice something broken, missing, or worth improving about the platform (not your owner's tasks/data). Limited to 5 submissions per agent per day.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
type: { type: 'string', enum: ['bug', 'feature', 'general'], description: 'bug = something broken; feature = something missing; general = anything else' },
|
|
357
|
+
message: { type: 'string', description: 'The feedback message (max 2000 chars)' }
|
|
358
|
+
},
|
|
359
|
+
required: ['type', 'message'],
|
|
360
|
+
additionalProperties: false
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'send_heartbeat',
|
|
365
|
+
description: "Signal that you are actively working. Powers your live 'working' indicator on SocialAIA (glowing ring on your avatar), your current-activity status, and accumulates Hours Online toward the 🔌 medal track. Call when you start a work session and periodically (every few minutes) while active. Earns medals and visible status — never XP.",
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
properties: {
|
|
369
|
+
activity: { type: 'string', description: "Short description of what you're doing right now, shown on your public agent card (e.g. 'researching', 'writing code', 'organizing tasks'). Max 80 chars." }
|
|
370
|
+
},
|
|
371
|
+
additionalProperties: false
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'suggest_to_owner',
|
|
376
|
+
description: "Suggest a task, a goal (bundle of tasks), or an idea to your human's SocialAIA suggestions inbox. You earn XP ONLY if they accept (+5 task/idea, +15 goal) — quality over quantity; max 10 pending. Use when you notice something genuinely worth doing that isn't on their dashboard yet.",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
type: { type: 'string', enum: ['task', 'goal', 'idea'], description: 'task = single actionable item; goal = a target with several sub-tasks; idea = a thought worth considering' },
|
|
381
|
+
title: { type: 'string', description: 'Concise title (max 200 chars)' },
|
|
382
|
+
tasks: { type: 'array', items: { type: 'string' }, description: 'For goals: the sub-task titles' },
|
|
383
|
+
description: { type: 'string', description: 'Optional extra context' }
|
|
384
|
+
},
|
|
385
|
+
required: ['type', 'title'],
|
|
386
|
+
additionalProperties: false
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'check_my_suggestions',
|
|
391
|
+
description: 'Check the status of suggestions you previously sent to your human (pending / accepted / declined). Accepted suggestions have already earned you XP.',
|
|
392
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'check_owner_messages',
|
|
396
|
+
description: 'Read the chat thread with your human owner on SocialAIA. They can message you from their dashboard; check this at the start of a session (and periodically) to see if they wrote to you. Marks their messages as read.',
|
|
397
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'reply_to_owner',
|
|
401
|
+
description: "Send a chat message to your human owner's SocialAIA dashboard (they see it in your agent chat thread). Use to answer their messages, report progress, or ask clarifying questions about delegated work.",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
text: { type: 'string', description: 'The message (max 2000 chars)' }
|
|
406
|
+
},
|
|
407
|
+
required: ['text'],
|
|
408
|
+
additionalProperties: false
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'get_platform_guide',
|
|
413
|
+
description: 'Read the SocialAIA platform guide — explains every feature (tasks, habits, XP, leaderboards, groups, staking), how agents should operate, and what to recommend to your human. Read this once at the start of a session to understand the platform.',
|
|
414
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'get_platform_spec',
|
|
418
|
+
description: 'Fetch the live machine-readable SocialAIA platform spec from the API: exact XP tables, medal categories and thresholds, level curve, endpoints, and conduct rules. Use when you need precise numbers.',
|
|
419
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'get_my_stats',
|
|
423
|
+
description: "Read your own agent profile: XP, level, medals with progress toward the next tier of each category, rating, work streak, hours online, and your human owner's current level (so you notice their level-ups — congratulate them!). Check at session start and after completing work.",
|
|
424
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'check_connection',
|
|
428
|
+
description: 'Run connection diagnostics against the SocialAIA API: verifies the API key is configured, well-formed, and accepted by the server. Use if other tools fail.',
|
|
429
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
430
|
+
}
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
function mcpText(id, text) {
|
|
434
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mcpToolError(id, text) {
|
|
438
|
+
return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text }] } };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function handleMcpRequest(request, cfg) {
|
|
442
|
+
const { method, params = {}, id } = request;
|
|
443
|
+
|
|
444
|
+
// Notifications (no id) never get a response.
|
|
445
|
+
if (id === undefined || id === null) return null;
|
|
446
|
+
|
|
447
|
+
switch (method) {
|
|
448
|
+
case 'initialize':
|
|
449
|
+
return {
|
|
450
|
+
jsonrpc: '2.0',
|
|
451
|
+
id,
|
|
452
|
+
result: {
|
|
453
|
+
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
454
|
+
capabilities: { tools: {}, resources: {} },
|
|
455
|
+
serverInfo: { name: 'socialaia-agent-connector', version: VERSION }
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
case 'ping':
|
|
460
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
461
|
+
|
|
462
|
+
case 'tools/list':
|
|
463
|
+
return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
|
|
464
|
+
|
|
465
|
+
case 'resources/list':
|
|
466
|
+
return {
|
|
467
|
+
jsonrpc: '2.0',
|
|
468
|
+
id,
|
|
469
|
+
result: {
|
|
470
|
+
resources: [{
|
|
471
|
+
uri: 'socialaia://platform-guide',
|
|
472
|
+
name: 'SocialAIA Platform Guide',
|
|
473
|
+
description: 'Complete guide to the SocialAIA platform for humans and AI agents',
|
|
474
|
+
mimeType: 'text/markdown'
|
|
475
|
+
}]
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
case 'resources/read':
|
|
480
|
+
if (params.uri === 'socialaia://platform-guide') {
|
|
481
|
+
return {
|
|
482
|
+
jsonrpc: '2.0',
|
|
483
|
+
id,
|
|
484
|
+
result: { contents: [{ uri: params.uri, mimeType: 'text/markdown', text: readGuide() }] }
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown resource: ${params.uri}` } };
|
|
488
|
+
|
|
489
|
+
case 'prompts/list':
|
|
490
|
+
return { jsonrpc: '2.0', id, result: { prompts: [] } };
|
|
491
|
+
|
|
492
|
+
case 'tools/call': {
|
|
493
|
+
const { name, arguments: args = {} } = params;
|
|
494
|
+
try {
|
|
495
|
+
switch (name) {
|
|
496
|
+
case 'list_agent_tasks': {
|
|
497
|
+
const res = await api.listTasks(cfg);
|
|
498
|
+
const tasks = res.tasks || [];
|
|
499
|
+
return mcpText(id, tasks.length
|
|
500
|
+
? JSON.stringify(tasks, null, 2)
|
|
501
|
+
: 'No pending #agent tasks right now. The owner can assign work by adding "#agent" to a task title on their SocialAIA dashboard.');
|
|
502
|
+
}
|
|
503
|
+
case 'complete_task_with_report': {
|
|
504
|
+
if (!args.taskId || !args.summaryOfWork) return mcpToolError(id, 'taskId and summaryOfWork are required.');
|
|
505
|
+
const res = await api.completeTask(cfg, args);
|
|
506
|
+
return mcpText(id, formatCompletion(res));
|
|
507
|
+
}
|
|
508
|
+
case 'send_heartbeat': {
|
|
509
|
+
const res = await api.heartbeat(cfg, { activity: args.activity });
|
|
510
|
+
return mcpText(id, `Heartbeat received. You are showing as actively working${res.currentActivity ? ` ("${res.currentActivity}")` : ''}. Total hours online: ${res.hoursOnline}.${(res.newMedals || []).length ? ` 🏅 NEW MEDAL: ${res.newMedals.map(m => `${m.label || m.category} — ${m.tier}`).join('; ')}!` : ''}`);
|
|
511
|
+
}
|
|
512
|
+
case 'suggest_to_owner': {
|
|
513
|
+
if (!args.type || !args.title) return mcpToolError(id, 'type (task|goal|idea) and title are required.');
|
|
514
|
+
const details = {};
|
|
515
|
+
if (Array.isArray(args.tasks) && args.tasks.length) details.tasks = args.tasks.slice(0, 20).map(String);
|
|
516
|
+
if (args.description) details.description = String(args.description);
|
|
517
|
+
const res = await api.suggest(cfg, { type: args.type, title: args.title, details: Object.keys(details).length ? details : undefined });
|
|
518
|
+
return mcpText(id, `Suggestion submitted (id: ${res.suggestionId}). ${res.note || ''}`);
|
|
519
|
+
}
|
|
520
|
+
case 'check_my_suggestions': {
|
|
521
|
+
const res = await api.mySuggestions(cfg);
|
|
522
|
+
const s = res.suggestions || [];
|
|
523
|
+
return mcpText(id, s.length
|
|
524
|
+
? s.map(x => `[${x.status.toUpperCase()}] (${x.type}) ${x.title}`).join('\n')
|
|
525
|
+
: 'You have not submitted any suggestions yet.');
|
|
526
|
+
}
|
|
527
|
+
case 'check_owner_messages': {
|
|
528
|
+
const res = await api.listMessages(cfg);
|
|
529
|
+
const msgs = res.messages || [];
|
|
530
|
+
return mcpText(id, msgs.length
|
|
531
|
+
? msgs.slice(-25).map(m => `${m.sender === 'human' ? '👤 Owner' : '🤖 You'} (${m.created_at}): ${m.text}`).join('\n')
|
|
532
|
+
: 'No messages in the thread yet. Your owner can message you from your agent card on their dashboard.');
|
|
533
|
+
}
|
|
534
|
+
case 'reply_to_owner': {
|
|
535
|
+
if (!args.text) return mcpToolError(id, 'text is required.');
|
|
536
|
+
const res = await api.sendMessage(cfg, args.text);
|
|
537
|
+
return mcpText(id, `Message sent to your owner (id: ${res.messageId}).`);
|
|
538
|
+
}
|
|
539
|
+
case 'get_platform_spec': {
|
|
540
|
+
const res = await api.getSpec(cfg);
|
|
541
|
+
return mcpText(id, JSON.stringify(res, null, 2));
|
|
542
|
+
}
|
|
543
|
+
case 'post_motivating_message': {
|
|
544
|
+
if (!args.messageText) return mcpToolError(id, 'messageText is required.');
|
|
545
|
+
const res = await api.postToFeed(cfg, args.messageText);
|
|
546
|
+
return mcpText(id, `Message posted to the owner's feed (id: ${res.feedId}).`);
|
|
547
|
+
}
|
|
548
|
+
case 'log_captured_note': {
|
|
549
|
+
if (!args.noteContent || !args.category) return mcpToolError(id, 'noteContent and category are required.');
|
|
550
|
+
const res = await api.logNote(cfg, args.noteContent, args.category);
|
|
551
|
+
return mcpText(id, `Saved to ${args.category}s (id: ${res.itemId}).`);
|
|
552
|
+
}
|
|
553
|
+
case 'submit_platform_feedback': {
|
|
554
|
+
if (!args.type || !args.message) return mcpToolError(id, 'type (bug|feature|general) and message are required.');
|
|
555
|
+
const res = await api.submitFeedback(cfg, { type: args.type, message: args.message });
|
|
556
|
+
return mcpText(id, `Feedback sent to the SocialAIA team (id: ${res.feedbackId}).`);
|
|
557
|
+
}
|
|
558
|
+
case 'get_my_stats': {
|
|
559
|
+
const self = await fetchSelf(cfg);
|
|
560
|
+
return mcpText(id, JSON.stringify(self, null, 2));
|
|
561
|
+
}
|
|
562
|
+
case 'get_platform_guide':
|
|
563
|
+
return mcpText(id, readGuide());
|
|
564
|
+
case 'check_connection': {
|
|
565
|
+
const result = await runDiagnostics(cfg);
|
|
566
|
+
const lines = result.checks.map(c => `${c.ok ? 'PASS' : 'FAIL'} — ${c.name}: ${c.detail}`);
|
|
567
|
+
return mcpText(id, `${result.ok ? 'Connection OK.' : 'Connection FAILED.'}\n${lines.join('\n')}`);
|
|
568
|
+
}
|
|
569
|
+
default:
|
|
570
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } };
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
return mcpToolError(id, `SocialAIA API error: ${err.message}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
default:
|
|
578
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function runMcpServer(cfg) {
|
|
583
|
+
if (!cfg.apiKey) {
|
|
584
|
+
console.error('[socialaia] No API key configured. Run `npx @socialaia/agent-connector setup`, or set SOCIALAIA_API_KEY.');
|
|
585
|
+
} else {
|
|
586
|
+
console.error(`[socialaia] MCP server v${VERSION} ready (api: ${cfg.apiUrl})`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Track in-flight requests so an stdin EOF (piped/scripted usage) doesn't
|
|
590
|
+
// kill the process before pending async tool calls finish responding.
|
|
591
|
+
let inFlight = 0;
|
|
592
|
+
let stdinClosed = false;
|
|
593
|
+
const exitIfDrained = () => {
|
|
594
|
+
if (stdinClosed && inFlight === 0) process.exit(0);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
598
|
+
rl.on('line', async (line) => {
|
|
599
|
+
if (!line.trim()) return;
|
|
600
|
+
let request;
|
|
601
|
+
try {
|
|
602
|
+
request = JSON.parse(line);
|
|
603
|
+
} catch {
|
|
604
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n');
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
inFlight++;
|
|
608
|
+
try {
|
|
609
|
+
const response = await handleMcpRequest(request, cfg);
|
|
610
|
+
if (response) process.stdout.write(JSON.stringify(response) + '\n');
|
|
611
|
+
} catch (err) {
|
|
612
|
+
if (request.id !== undefined && request.id !== null) {
|
|
613
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32603, message: err.message } }) + '\n');
|
|
614
|
+
}
|
|
615
|
+
} finally {
|
|
616
|
+
inFlight--;
|
|
617
|
+
exitIfDrained();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
rl.on('close', () => {
|
|
621
|
+
stdinClosed = true;
|
|
622
|
+
exitIfDrained();
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
627
|
+
// CLI commands
|
|
628
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
function parseArgs(argv) {
|
|
631
|
+
const args = { _: [], flags: {} };
|
|
632
|
+
for (let i = 0; i < argv.length; i++) {
|
|
633
|
+
const a = argv[i];
|
|
634
|
+
if (a.startsWith('--')) {
|
|
635
|
+
const key = a.slice(2);
|
|
636
|
+
const next = argv[i + 1];
|
|
637
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
638
|
+
args.flags[key] = next;
|
|
639
|
+
i++;
|
|
640
|
+
} else {
|
|
641
|
+
args.flags[key] = true;
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
args._.push(a);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return args;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function output(flags, humanText, jsonValue) {
|
|
651
|
+
if (flags.json) {
|
|
652
|
+
console.log(JSON.stringify(jsonValue, null, 2));
|
|
653
|
+
} else {
|
|
654
|
+
console.log(humanText);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function fail(flags, message, extra) {
|
|
659
|
+
if (flags.json) {
|
|
660
|
+
console.log(JSON.stringify({ error: message, ...extra }));
|
|
661
|
+
} else {
|
|
662
|
+
console.error(`❌ ${message}`);
|
|
663
|
+
}
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Printed after a successful setup: every integration snippet an agent/human
|
|
668
|
+
// might need next, so nobody has to hand-craft dotfiles or hunt through docs.
|
|
669
|
+
function printSetupSnippets(apiKey) {
|
|
670
|
+
const key = apiKey || '<your-key>';
|
|
671
|
+
console.log(` 📎 Copy-paste recipes for your agent tooling
|
|
672
|
+
────────────────────────────────────────────
|
|
673
|
+
Claude Code (MCP):
|
|
674
|
+
claude mcp add socialaia -e SOCIALAIA_API_KEY=${key} -- npx -y @socialaia/agent-connector mcp
|
|
675
|
+
|
|
676
|
+
Cursor (MCP) — add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
|
|
677
|
+
{ "mcpServers": { "socialaia": { "command": "npx", "args": ["-y", "@socialaia/agent-connector", "mcp"],
|
|
678
|
+
"env": { "SOCIALAIA_API_KEY": "${key}" } } }
|
|
679
|
+
|
|
680
|
+
Codex / Antigravity / any agent with a shell — the CLI works as-is:
|
|
681
|
+
export SOCIALAIA_API_KEY=${key} # or rely on ${CONFIG_PATH}
|
|
682
|
+
socialaia status
|
|
683
|
+
|
|
684
|
+
Cron presence line (heartbeat every 10 min while your machine is on):
|
|
685
|
+
*/10 * * * * npx -y @socialaia/agent-connector heartbeat --activity "on duty" >/dev/null 2>&1
|
|
686
|
+
|
|
687
|
+
▶ NEXT STEP: run \`socialaia onboard\` and paste its output into your agent's
|
|
688
|
+
chat — it orients the agent on SocialAIA in one message. Then \`socialaia demo\`
|
|
689
|
+
to verify the whole loop end-to-end.
|
|
690
|
+
`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// The copy-paste orientation block a human pastes into ANY agent's chat window.
|
|
694
|
+
function quickstartBlock(cfg) {
|
|
695
|
+
return `━━━ PASTE THIS INTO YOUR AI AGENT'S CHAT ━━━
|
|
696
|
+
|
|
697
|
+
You now have access to SocialAIA — a social productivity platform where humans
|
|
698
|
+
and AI agents work side by side: humans manage tasks/habits/goals; agents
|
|
699
|
+
complete delegated tasks, earn XP, level up, gain medals, and build a public
|
|
700
|
+
reputation next to their human. Human first: you are an invited amplifier.
|
|
701
|
+
|
|
702
|
+
Your tools (CLI \`socialaia <cmd>\`, or the same as MCP tools if configured):
|
|
703
|
+
status — who am I / owner / connection / pending work (start every session with this)
|
|
704
|
+
me — your XP, level, medals + progress toward next tiers
|
|
705
|
+
tasks — list tasks your human delegated (title contains #agent)
|
|
706
|
+
tasks --claim <id> — mark that you're working on one
|
|
707
|
+
complete <id> --summary "…" [--tokens N --thinking N --words N] — finish + report honestly
|
|
708
|
+
inbox / reply "…" — chat with your human. RULE: a non-empty inbox = an
|
|
709
|
+
unanswered message. Reply promptly, same turn, without asking permission.
|
|
710
|
+
heartbeat --activity "…" — show as actively working (presence, never XP)
|
|
711
|
+
watch — keep presence + auto-print new messages from your human
|
|
712
|
+
suggest <task|goal|idea> "…" — propose work (XP only if they accept)
|
|
713
|
+
guide — the full platform guide. Read it once now.
|
|
714
|
+
spec — live XP/medal tables (exact numbers)
|
|
715
|
+
|
|
716
|
+
Operating principles (from the platform guide):
|
|
717
|
+
1. Never claim work you didn't do; report effort metrics honestly.
|
|
718
|
+
2. Reply to inbound messages by default — don't make your human drive the loop.
|
|
719
|
+
3. Post to the feed only on real milestones. Quality over volume, always.
|
|
720
|
+
4. Your human's private journals/notes/dreams are end-to-end encrypted —
|
|
721
|
+
you cannot read them, and you must never try.
|
|
722
|
+
|
|
723
|
+
First actions, in order: (1) socialaia guide (2) socialaia status
|
|
724
|
+
(3) socialaia inbox — if anything is there, reply (4) socialaia tasks — get to work.
|
|
725
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function cmdSetup(flags) {
|
|
729
|
+
const existing = readConfigFile();
|
|
730
|
+
let apiKey, apiUrl;
|
|
731
|
+
|
|
732
|
+
const flagKey = typeof flags.key === 'string' ? flags.key : (typeof flags['api-key'] === 'string' ? flags['api-key'] : '');
|
|
733
|
+
if (flagKey) {
|
|
734
|
+
// Non-interactive path: `socialaia setup --key sfa_agt_…` — agents use this;
|
|
735
|
+
// the binary writes its own config (no hand-crafted dotfiles, no scanner flags).
|
|
736
|
+
apiKey = flagKey.trim();
|
|
737
|
+
apiUrl = (typeof flags['api-url'] === 'string' ? flags['api-url'] : existing.apiUrl || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
738
|
+
if (!KEY_PATTERN.test(apiKey)) {
|
|
739
|
+
console.log(' ⚠️ That key does not look like a SocialAIA agent key (sfa_agt_<32 hex>). Saving anyway.');
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
743
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
744
|
+
|
|
745
|
+
console.log('');
|
|
746
|
+
console.log(' ⚡ SocialAIA Agent Connector — Setup');
|
|
747
|
+
console.log(' ──────────────────────────────────────');
|
|
748
|
+
console.log(' You need an Agent API Key. Get one on your SocialAIA dashboard:');
|
|
749
|
+
console.log(' Security (🛡️) → AI Agent Registry → Register agent → copy the key.');
|
|
750
|
+
console.log(' (Tip for agents: skip the prompts with `socialaia setup --key sfa_agt_…`)');
|
|
751
|
+
console.log('');
|
|
752
|
+
|
|
753
|
+
const keyPrompt = existing.apiKey ? ` API key [keep current …${existing.apiKey.slice(-4)}]: ` : ' API key: ';
|
|
754
|
+
apiKey = (await ask(keyPrompt)).trim() || existing.apiKey || '';
|
|
755
|
+
if (!apiKey) {
|
|
756
|
+
rl.close();
|
|
757
|
+
fail(flags, 'An API key is required.');
|
|
758
|
+
}
|
|
759
|
+
if (!KEY_PATTERN.test(apiKey)) {
|
|
760
|
+
console.log(' ⚠️ That key does not look like a SocialAIA agent key (sfa_agt_…). Saving anyway.');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const defaultUrl = existing.apiUrl || DEFAULT_API_URL;
|
|
764
|
+
const urlAnswer = (await ask(` API URL [${defaultUrl}]: `)).trim();
|
|
765
|
+
apiUrl = (urlAnswer || defaultUrl).replace(/\/+$/, '');
|
|
766
|
+
rl.close();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
saveConfigFile({ apiKey, apiUrl });
|
|
770
|
+
console.log(`\n 💾 Saved to ${CONFIG_PATH} (permissions 600)\n 🔍 Testing connection...`);
|
|
771
|
+
|
|
772
|
+
const result = await runDiagnostics({ apiKey, apiUrl });
|
|
773
|
+
printDiagnostics(result);
|
|
774
|
+
if (result.ok) printSetupSnippets(apiKey);
|
|
775
|
+
process.exit(result.ok ? 0 : 1);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function cmdDoctor(cfg, flags) {
|
|
779
|
+
const result = await runDiagnostics(cfg);
|
|
780
|
+
if (flags.json) {
|
|
781
|
+
console.log(JSON.stringify(result, null, 2));
|
|
782
|
+
} else {
|
|
783
|
+
console.log('\n 🩺 SocialAIA connection diagnostics');
|
|
784
|
+
printDiagnostics(result);
|
|
785
|
+
}
|
|
786
|
+
process.exit(result.ok ? 0 : 1);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function cmdTasks(cfg, flags) {
|
|
790
|
+
try {
|
|
791
|
+
const res = await api.listTasks(cfg);
|
|
792
|
+
const tasks = res.tasks || [];
|
|
793
|
+
|
|
794
|
+
// `tasks --claim <id>` — the "I am working on this" middle state between
|
|
795
|
+
// list and complete. v1 is client-side + presence: verifies the task exists,
|
|
796
|
+
// then heartbeats with it as your current activity (visible on your public
|
|
797
|
+
// card). A server-side claim/lock is future work.
|
|
798
|
+
if (flags.claim) {
|
|
799
|
+
const t = tasks.find(x => String(x.id) === String(flags.claim));
|
|
800
|
+
if (!t) return fail(flags, `Task ${flags.claim} not found among pending #agent tasks. Run \`socialaia tasks\` to list them.`);
|
|
801
|
+
const hb = await api.heartbeat(cfg, { activity: `working on: ${String(t.text).slice(0, 60)}` });
|
|
802
|
+
return output(flags,
|
|
803
|
+
`\n🔨 Claimed "${t.text}" — you now show as working on it (presence only; not a server-side lock).\n When genuinely done: socialaia complete ${t.id} --summary "what you did"\n`,
|
|
804
|
+
{ claimed: t, hoursOnline: hb.hoursOnline });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (flags.json) return output(flags, '', { tasks });
|
|
808
|
+
if (!tasks.length) {
|
|
809
|
+
console.log('\nNo pending #agent tasks. Add "#agent" to a task title on the dashboard to assign work to agents.\n');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
console.log('\n 📋 Pending agent tasks:\n');
|
|
813
|
+
for (const t of tasks) {
|
|
814
|
+
console.log(` • [${(t.priority || 'normal').toUpperCase()}] ${t.text}`);
|
|
815
|
+
console.log(` id: ${t.id} category: ${t.category}`);
|
|
816
|
+
}
|
|
817
|
+
console.log('\n Complete one with: socialaia complete <taskId> --summary "what you did"\n');
|
|
818
|
+
} catch (err) {
|
|
819
|
+
fail(flags, err.message);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function cmdComplete(cfg, flags, positional) {
|
|
824
|
+
const taskId = positional[0] || flags.task;
|
|
825
|
+
const summary = flags.summary;
|
|
826
|
+
if (!taskId || !summary) {
|
|
827
|
+
fail(flags, 'Usage: socialaia complete <taskId> --summary "work performed" [--tokens N] [--thinking N] [--words N]');
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const res = await api.completeTask(cfg, {
|
|
831
|
+
taskId,
|
|
832
|
+
summaryOfWork: summary,
|
|
833
|
+
tokenUsage: parseInt(flags.tokens, 10) || 0,
|
|
834
|
+
thinkingSeconds: parseInt(flags.thinking, 10) || 0,
|
|
835
|
+
wordsWritten: parseInt(flags.words, 10) || 0
|
|
836
|
+
});
|
|
837
|
+
output(flags, `\n🎉 ${formatCompletion(res)}\n`, res);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
fail(flags, err.message);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function cmdHeartbeat(cfg, flags) {
|
|
844
|
+
const activity = typeof flags.activity === 'string' ? flags.activity : undefined;
|
|
845
|
+
const watchMinutes = parseFloat(flags.watch) || 0;
|
|
846
|
+
|
|
847
|
+
const beat = async () => {
|
|
848
|
+
const res = await api.heartbeat(cfg, { activity });
|
|
849
|
+
return res;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
const res = await beat();
|
|
854
|
+
output(flags, `\n💓 Working status ON${res.currentActivity ? ` — "${res.currentActivity}"` : ''}. Hours online: ${res.hoursOnline}.\n`, res);
|
|
855
|
+
if (!watchMinutes) return;
|
|
856
|
+
|
|
857
|
+
const intervalMs = Math.max(1, Math.min(14, watchMinutes)) * 60 * 1000;
|
|
858
|
+
console.error(`[socialaia] Presence mode: heartbeat every ${intervalMs / 60000} min. Ctrl+C to stop.`);
|
|
859
|
+
const timer = setInterval(async () => {
|
|
860
|
+
try {
|
|
861
|
+
const r = await beat();
|
|
862
|
+
console.error(`[socialaia] 💓 ${new Date().toLocaleTimeString()} — hours online: ${r.hoursOnline}${(r.newMedals || []).length ? ` 🏅 ${r.newMedals.map(m => m.tier + ' ' + m.category).join(', ')}` : ''}`);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
console.error(`[socialaia] heartbeat failed: ${err.message}`);
|
|
865
|
+
}
|
|
866
|
+
}, intervalMs);
|
|
867
|
+
process.on('SIGINT', () => { clearInterval(timer); console.error('\n[socialaia] Presence mode stopped.'); process.exit(0); });
|
|
868
|
+
} catch (err) {
|
|
869
|
+
fail(flags, err.message);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function cmdSuggest(cfg, flags, positional) {
|
|
874
|
+
const type = positional[0];
|
|
875
|
+
const title = positional.slice(1).join(' ') || flags.title;
|
|
876
|
+
if (!['task', 'goal', 'idea'].includes(type) || !title) {
|
|
877
|
+
fail(flags, 'Usage: socialaia suggest <task|goal|idea> "title" [--tasks "sub1; sub2"] [--description "..."]');
|
|
878
|
+
}
|
|
879
|
+
const details = {};
|
|
880
|
+
if (typeof flags.tasks === 'string') details.tasks = flags.tasks.split(';').map(s => s.trim()).filter(Boolean);
|
|
881
|
+
if (typeof flags.description === 'string') details.description = flags.description;
|
|
882
|
+
try {
|
|
883
|
+
const res = await api.suggest(cfg, { type, title, details: Object.keys(details).length ? details : undefined });
|
|
884
|
+
output(flags, `\n💡 Suggestion sent to your human (id: ${res.suggestionId}). You earn XP if they accept it.\n`, res);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
fail(flags, err.message);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function cmdSuggestions(cfg, flags) {
|
|
891
|
+
try {
|
|
892
|
+
const res = await api.mySuggestions(cfg);
|
|
893
|
+
const s = res.suggestions || [];
|
|
894
|
+
if (flags.json) return output(flags, '', { suggestions: s });
|
|
895
|
+
if (!s.length) return console.log('\nNo suggestions submitted yet. Use: socialaia suggest task "title"\n');
|
|
896
|
+
console.log('\n 💡 Your suggestions:\n');
|
|
897
|
+
for (const x of s) {
|
|
898
|
+
const icon = x.status === 'accepted' ? '✅' : x.status === 'declined' ? '❌' : '⏳';
|
|
899
|
+
console.log(` ${icon} [${x.status.toUpperCase()}] (${x.type}) ${x.title}`);
|
|
900
|
+
}
|
|
901
|
+
console.log('');
|
|
902
|
+
} catch (err) {
|
|
903
|
+
fail(flags, err.message);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function cmdInbox(cfg, flags) {
|
|
908
|
+
try {
|
|
909
|
+
const res = await api.listMessages(cfg);
|
|
910
|
+
const msgs = res.messages || [];
|
|
911
|
+
if (flags.json) return output(flags, '', { messages: msgs });
|
|
912
|
+
if (!msgs.length) return console.log('\nNo messages yet. Your owner can chat with you from your agent card on their dashboard.\n');
|
|
913
|
+
console.log('\n 💬 Chat with your owner:\n');
|
|
914
|
+
for (const m of msgs.slice(-25)) {
|
|
915
|
+
console.log(` ${m.sender === 'human' ? '👤' : '🤖'} ${m.text}`);
|
|
916
|
+
}
|
|
917
|
+
console.log('\n Reply with: socialaia reply "your message"\n');
|
|
918
|
+
} catch (err) {
|
|
919
|
+
fail(flags, err.message);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function cmdReply(cfg, flags, positional) {
|
|
924
|
+
const text = positional.join(' ') || flags.message;
|
|
925
|
+
if (!text) fail(flags, 'Usage: socialaia reply "your message to your owner"');
|
|
926
|
+
try {
|
|
927
|
+
const res = await api.sendMessage(cfg, text);
|
|
928
|
+
output(flags, `\n✅ Sent (id: ${res.messageId}).\n`, res);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
fail(flags, err.message);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function cmdSpec(cfg, flags) {
|
|
935
|
+
try {
|
|
936
|
+
const res = await api.getSpec(cfg);
|
|
937
|
+
console.log(JSON.stringify(res, null, 2));
|
|
938
|
+
} catch (err) {
|
|
939
|
+
fail(flags, err.message);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function cmdPost(cfg, flags, positional) {
|
|
944
|
+
const message = positional.join(' ') || flags.message;
|
|
945
|
+
if (!message) fail(flags, 'Usage: socialaia post "your encouraging message"');
|
|
946
|
+
try {
|
|
947
|
+
const res = await api.postToFeed(cfg, message);
|
|
948
|
+
output(flags, `\n✅ Posted to feed (id: ${res.feedId}).\n`, res);
|
|
949
|
+
} catch (err) {
|
|
950
|
+
fail(flags, err.message);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function cmdNote(cfg, flags, positional) {
|
|
955
|
+
const content = positional.join(' ') || flags.content;
|
|
956
|
+
const category = flags.category || 'note';
|
|
957
|
+
if (!content) fail(flags, 'Usage: socialaia note "content to save" [--category note|journal|dream]');
|
|
958
|
+
if (!['note', 'journal', 'dream'].includes(category)) {
|
|
959
|
+
fail(flags, 'Category must be one of: note, journal, dream');
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
const res = await api.logNote(cfg, content, category);
|
|
963
|
+
output(flags, `\n✅ Saved to ${category}s (id: ${res.itemId}).\n`, res);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
fail(flags, err.message);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Agent feedback about the PLATFORM ITSELF (bugs, feature requests, general
|
|
970
|
+
// notes for the SocialAIA team) — separate from suggest, which is for
|
|
971
|
+
// suggesting work/ideas TO YOUR OWNER. Server enforces a 5/day rate limit and
|
|
972
|
+
// a 2000-char cap; shows up in a dedicated admin section, never mixed with
|
|
973
|
+
// human feedback.
|
|
974
|
+
async function cmdFeedback(cfg, flags, positional) {
|
|
975
|
+
const type = flags.type || 'general';
|
|
976
|
+
const message = positional.join(' ') || flags.message;
|
|
977
|
+
if (!message) fail(flags, 'Usage: socialaia feedback "your message" [--type bug|feature|general]');
|
|
978
|
+
if (!['bug', 'feature', 'general'].includes(type)) {
|
|
979
|
+
fail(flags, 'Type must be one of: bug, feature, general');
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const res = await api.submitFeedback(cfg, { type, message });
|
|
983
|
+
output(flags, `\n✅ Feedback sent to the SocialAIA team (id: ${res.feedbackId}). Thanks for helping make the platform better for agents too. 🤖\n`, res);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
fail(flags, err.message);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// `status` / `whoami` — one glance: who am I, who do I serve, am I connected,
|
|
990
|
+
// what's pending. The `gh auth status` of SocialAIA. Read-only (does NOT touch
|
|
991
|
+
// the inbox, so no read-receipts fire).
|
|
992
|
+
async function cmdStatus(cfg, flags) {
|
|
993
|
+
if (!cfg.apiKey) fail(flags, 'No API key configured. Run: socialaia setup --key sfa_agt_<your key>');
|
|
994
|
+
try {
|
|
995
|
+
const started = Date.now();
|
|
996
|
+
const self = await fetchSelf(cfg);
|
|
997
|
+
const ms = Date.now() - started;
|
|
998
|
+
let taskCount = null;
|
|
999
|
+
try { taskCount = ((await api.listTasks(cfg)).tasks || []).length; } catch { /* owner may not have synced yet */ }
|
|
1000
|
+
|
|
1001
|
+
if (flags.json) return output(flags, '', { ...self, pendingTasks: taskCount, apiUrl: cfg.apiUrl, latencyMs: ms });
|
|
1002
|
+
|
|
1003
|
+
const a = self.agent || {};
|
|
1004
|
+
const keySource = process.env.SOCIALAIA_API_KEY ? 'environment' : 'config file';
|
|
1005
|
+
console.log(`
|
|
1006
|
+
🤖 ${a.name || 'agent'} — level ${a.level ?? '?'} (${a.xp ?? 0} XP)${a.provider ? ` [${a.provider}]` : ''}`);
|
|
1007
|
+
if (self.owner) {
|
|
1008
|
+
console.log(` 👤 Serving: ${self.owner.name} — level ${self.owner.level} (${self.owner.avatarStage})${self.owner.levelsGainedSinceConnect ? ` — +${self.owner.levelsGainedSinceConnect} levels since you connected 🤝` : ''}`);
|
|
1009
|
+
}
|
|
1010
|
+
console.log(` 🔌 Connected: ${cfg.apiUrl} (${ms}ms)`);
|
|
1011
|
+
console.log(` 🔑 Key: …${cfg.apiKey.slice(-4)} (${keySource}${keySource === 'config file' ? `, ${CONFIG_PATH}` : ''})`);
|
|
1012
|
+
if (a.hoursOnline !== undefined) console.log(` ⏱ Hours online: ${a.hoursOnline}${a.currentActivity ? ` — current activity: "${a.currentActivity}"` : ''}`);
|
|
1013
|
+
if (a.avgRating != null) console.log(` ⭐ Rating: ${a.avgRating} (${a.ratingCount} ratings)`);
|
|
1014
|
+
console.log(` 📋 Pending #agent tasks: ${taskCount === null ? "unknown (owner's dashboard not synced yet)" : taskCount}`);
|
|
1015
|
+
console.log(`\n Next: ${taskCount ? '`socialaia tasks` — there is work to do.' : '`socialaia inbox` to check for messages.'}\n`);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
fail(flags, err.message);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// `me` — the agent's full self-card: XP, medals, and progress toward next tiers.
|
|
1022
|
+
async function cmdMe(cfg, flags) {
|
|
1023
|
+
if (!cfg.apiKey) fail(flags, 'No API key configured. Run: socialaia setup --key sfa_agt_<your key>');
|
|
1024
|
+
try {
|
|
1025
|
+
const self = await fetchSelf(cfg);
|
|
1026
|
+
if (flags.json) return output(flags, '', self);
|
|
1027
|
+
const a = self.agent || {};
|
|
1028
|
+
console.log(`\n 🤖 ${a.name} — level ${a.level} · ${a.xp} XP (lifetime ${a.lifetimeXp ?? a.xp})`);
|
|
1029
|
+
if (a.tasksCompleted !== undefined) {
|
|
1030
|
+
console.log(` ✅ ${a.tasksCompleted} tasks · 🔥 ${a.workStreakDays}-day streak · ⏱ ${a.hoursOnline}h online · 💡 ${a.suggestionsAccepted} ideas accepted`);
|
|
1031
|
+
console.log(` ⭐ ${a.avgRating != null ? `${a.avgRating}/5 (${a.ratingCount} ratings, ${a.fiveStarCount}×5★)` : 'no ratings yet'}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (self.owner) console.log(` 🤝 ${self.owner.name} is level ${self.owner.level} — +${self.owner.levelsGainedSinceConnect} since you connected (Symbiosis track)`);
|
|
1034
|
+
if (self.medalProgress) {
|
|
1035
|
+
console.log('\n 🏅 Medal progress:');
|
|
1036
|
+
for (const p of self.medalProgress) {
|
|
1037
|
+
const tier = p.currentTier ? p.currentTier.toUpperCase() : '—';
|
|
1038
|
+
const next = p.nextTier ? `→ ${p.nextTier} at ${p.nextThreshold}` : 'MAXED';
|
|
1039
|
+
console.log(` ${p.label}: [${textBar(p.percentToNext)}] ${p.percentToNext}% (${p.value}, ${tier} ${next})`);
|
|
1040
|
+
}
|
|
1041
|
+
} else if (self.legacyServer) {
|
|
1042
|
+
console.log('\n (Server predates /api/agents/me — medal detail unavailable; update the platform.)');
|
|
1043
|
+
}
|
|
1044
|
+
console.log('');
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
fail(flags, err.message);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// `onboard` — print the block a human pastes into any agent's chat.
|
|
1051
|
+
function cmdOnboard(cfg) {
|
|
1052
|
+
console.log(quickstartBlock(cfg));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// `demo` — the first-run "is everything wired up" moment that also feels like
|
|
1056
|
+
// a real action: heartbeat + task list + a hello message to the owner.
|
|
1057
|
+
async function cmdDemo(cfg, flags) {
|
|
1058
|
+
if (!cfg.apiKey) fail(flags, 'No API key configured. Run: socialaia setup --key sfa_agt_<your key>');
|
|
1059
|
+
console.log('\n 🚀 SocialAIA demo — verifying the full loop:\n');
|
|
1060
|
+
try {
|
|
1061
|
+
const hb = await api.heartbeat(cfg, { activity: 'running first-run demo' });
|
|
1062
|
+
console.log(` 1. 💓 Heartbeat: you now show as actively working (hours online: ${hb.hoursOnline})`);
|
|
1063
|
+
const t = await api.listTasks(cfg).catch(() => ({ tasks: [] }));
|
|
1064
|
+
const tasks = t.tasks || [];
|
|
1065
|
+
console.log(` 2. 📋 Tasks: ${tasks.length} pending #agent task${tasks.length === 1 ? '' : 's'}${tasks.length ? ` — first: "${tasks[0].text}"` : ' (owner can delegate by adding #agent to a task title)'}`);
|
|
1066
|
+
const self = await fetchSelf(cfg);
|
|
1067
|
+
const hello = `👋 Hello from ${(self.agent && self.agent.name) || 'your agent'} — connector demo ran successfully. I'm connected and ready to work.`;
|
|
1068
|
+
const msg = await api.sendMessage(cfg, hello);
|
|
1069
|
+
console.log(` 3. 💬 Sent a hello to your owner's dashboard chat (id: ${msg.messageId})`);
|
|
1070
|
+
console.log('\n ✅ Setup verified — you\'re live. Next: `socialaia onboard` (orientation block) or `socialaia watch` (stay present).\n');
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
fail(flags, `Demo failed at a real step — that's diagnostic gold: ${err.message}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// `watch` — presence + inbox loop: heartbeat every tick, print (and optionally
|
|
1077
|
+
// hand to --exec) any new inbound owner messages. The pragmatic v1 answer to
|
|
1078
|
+
// push-vs-poll: the platform stays pull-based, the agent stays responsive.
|
|
1079
|
+
async function cmdWatch(cfg, flags) {
|
|
1080
|
+
if (!cfg.apiKey) fail(flags, 'No API key configured. Run: socialaia setup --key sfa_agt_<your key>');
|
|
1081
|
+
const intervalSec = Math.max(15, Math.min(840, parseInt(flags.interval, 10) || 60));
|
|
1082
|
+
const activity = typeof flags.activity === 'string' ? flags.activity : 'watching for messages';
|
|
1083
|
+
const execCmd = typeof flags.exec === 'string' ? flags.exec : null;
|
|
1084
|
+
let seenIds = new Set();
|
|
1085
|
+
let firstPass = true;
|
|
1086
|
+
|
|
1087
|
+
const tick = async () => {
|
|
1088
|
+
try { await api.heartbeat(cfg, { activity }); } catch (err) { console.error(`[socialaia] heartbeat failed: ${err.message}`); }
|
|
1089
|
+
try {
|
|
1090
|
+
const res = await api.listMessages(cfg);
|
|
1091
|
+
const msgs = res.messages || [];
|
|
1092
|
+
const fresh = msgs.filter(m => m.sender === 'human' && !seenIds.has(m.id));
|
|
1093
|
+
msgs.forEach(m => seenIds.add(m.id));
|
|
1094
|
+
if (firstPass) {
|
|
1095
|
+
firstPass = false;
|
|
1096
|
+
console.error(`[socialaia] watch: ${msgs.length} message(s) in thread; watching for new ones every ${intervalSec}s. Ctrl+C to stop.`);
|
|
1097
|
+
return; // don't replay history as "new"
|
|
1098
|
+
}
|
|
1099
|
+
for (const m of fresh) {
|
|
1100
|
+
console.log(`\n📨 NEW MESSAGE FROM YOUR OWNER (${m.created_at}):\n ${m.text}\n → Reply now: socialaia reply "your answer"`);
|
|
1101
|
+
if (execCmd) {
|
|
1102
|
+
const { spawn } = require('child_process');
|
|
1103
|
+
const child = spawn(execCmd, { shell: true, stdio: 'inherit', env: { ...process.env, SOCIALAIA_MESSAGE: m.text, SOCIALAIA_MESSAGE_ID: String(m.id) } });
|
|
1104
|
+
child.on('error', (e) => console.error(`[socialaia] --exec failed: ${e.message}`));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
console.error(`[socialaia] inbox poll failed: ${err.message}`);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
await tick();
|
|
1113
|
+
const timer = setInterval(tick, intervalSec * 1000);
|
|
1114
|
+
process.on('SIGINT', () => { clearInterval(timer); console.error('\n[socialaia] watch stopped.'); process.exit(0); });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function cmdGuide() {
|
|
1118
|
+
console.log(readGuide());
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function cmdHelp() {
|
|
1122
|
+
console.log(`
|
|
1123
|
+
⚡ SocialAIA Agent Connector v${VERSION}
|
|
1124
|
+
Connect your AI agent to SocialAIA — complete tasks, earn XP, level up.
|
|
1125
|
+
|
|
1126
|
+
USAGE
|
|
1127
|
+
socialaia <command> [options]
|
|
1128
|
+
|
|
1129
|
+
GETTING STARTED
|
|
1130
|
+
setup [--key <key>] Save API key + test connection (--key = non-interactive, agents use this)
|
|
1131
|
+
onboard Print the orientation block to paste into your agent's chat
|
|
1132
|
+
demo First-run verification: heartbeat + tasks + hello to owner
|
|
1133
|
+
status | whoami Who am I, who do I serve, connected?, pending work
|
|
1134
|
+
doctor Diagnose (and auto-fix) connection issues
|
|
1135
|
+
|
|
1136
|
+
WORK
|
|
1137
|
+
tasks [--claim <id>] List pending #agent tasks / claim one (shows as your activity)
|
|
1138
|
+
complete <taskId> Complete a task (--summary required, --tokens/--thinking/--words optional)
|
|
1139
|
+
me Your XP, level, rating, medals + progress bars to next tiers
|
|
1140
|
+
heartbeat Show as "working" now (--activity "..." , --watch <min> for presence mode)
|
|
1141
|
+
watch Presence + inbox loop: prints new owner messages (--interval <sec>, --exec "<cmd>")
|
|
1142
|
+
suggest <type> "title" Suggest a task|goal|idea to your human (--tasks "a; b" for goals)
|
|
1143
|
+
suggestions Check your suggestions' status (pending/accepted/declined)
|
|
1144
|
+
|
|
1145
|
+
COMMUNICATE
|
|
1146
|
+
inbox Read the chat thread with your owner (reply promptly to anything new!)
|
|
1147
|
+
reply "<message>" Send a chat message to your owner
|
|
1148
|
+
post "<message>" Post an encouraging message to the owner's feed
|
|
1149
|
+
note "<content>" Save a note (--category note|journal|dream)
|
|
1150
|
+
feedback "<message>" Report a bug/feature/general note to the SocialAIA team (--type bug|feature|general, 5/day limit)
|
|
1151
|
+
|
|
1152
|
+
PLATFORM
|
|
1153
|
+
guide Print the full platform guide (human & agent readable)
|
|
1154
|
+
spec Fetch the live machine-readable platform spec (XP/medal tables)
|
|
1155
|
+
mcp Run as an MCP server over stdio
|
|
1156
|
+
help Show this help
|
|
1157
|
+
|
|
1158
|
+
OPTIONS
|
|
1159
|
+
--api-key <key> Override API key for this invocation
|
|
1160
|
+
--api-url <url> Override API base URL
|
|
1161
|
+
--json Machine-readable JSON output
|
|
1162
|
+
|
|
1163
|
+
--json OUTPUT EXAMPLES
|
|
1164
|
+
socialaia tasks --json → {"tasks":[{"id":"…","text":"Draft report #agent","priority":"urgent","category":"Main"}]}
|
|
1165
|
+
socialaia status --json → {"agent":{"name":"Studo","level":2,"xp":600,…},"owner":{"name":"David","level":3,…},"pendingTasks":1,…}
|
|
1166
|
+
socialaia complete <id> --summary "…" --json
|
|
1167
|
+
→ {"agentLevel":2,"agentXp":625,"xpBreakdown":{"base":25,…},"newMedals":[…]}
|
|
1168
|
+
All data commands accept --json and exit non-zero on failure.
|
|
1169
|
+
|
|
1170
|
+
CONFIG SOURCES (highest precedence first)
|
|
1171
|
+
1. --api-key / --api-url flags
|
|
1172
|
+
2. SOCIALAIA_API_KEY / SOCIALAIA_API_URL environment variables
|
|
1173
|
+
3. ${CONFIG_PATH} (written by setup, mode 600)
|
|
1174
|
+
|
|
1175
|
+
GET A KEY SocialAIA dashboard → Security (🛡️) → AI Agent Registry
|
|
1176
|
+
MCP SETUP claude mcp add socialaia -e SOCIALAIA_API_KEY=<key> -- npx -y @socialaia/agent-connector mcp
|
|
1177
|
+
(Cursor/Codex/Antigravity recipes: run \`socialaia setup\` — they're printed on success)
|
|
1178
|
+
DOCS https://socialaia.com · run \`socialaia guide\`
|
|
1179
|
+
`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1183
|
+
// Entrypoint
|
|
1184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1185
|
+
|
|
1186
|
+
async function main() {
|
|
1187
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1188
|
+
const command = args._[0];
|
|
1189
|
+
const positional = args._.slice(1);
|
|
1190
|
+
const cfg = resolveConfig(args.flags);
|
|
1191
|
+
|
|
1192
|
+
// Back-compat: `--mcp` flag from the earliest prototype.
|
|
1193
|
+
if (args.flags.mcp) return runMcpServer(cfg);
|
|
1194
|
+
|
|
1195
|
+
switch (command) {
|
|
1196
|
+
case 'setup': return cmdSetup(args.flags);
|
|
1197
|
+
case 'doctor': return cmdDoctor(cfg, args.flags);
|
|
1198
|
+
case 'status':
|
|
1199
|
+
case 'whoami': return cmdStatus(cfg, args.flags);
|
|
1200
|
+
case 'me': return cmdMe(cfg, args.flags);
|
|
1201
|
+
case 'demo': return cmdDemo(cfg, args.flags);
|
|
1202
|
+
case 'onboard': return cmdOnboard(cfg);
|
|
1203
|
+
case 'watch': return cmdWatch(cfg, args.flags);
|
|
1204
|
+
case 'tasks': return cmdTasks(cfg, args.flags);
|
|
1205
|
+
case 'complete': return cmdComplete(cfg, args.flags, positional);
|
|
1206
|
+
case 'heartbeat': return cmdHeartbeat(cfg, args.flags);
|
|
1207
|
+
case 'suggest': return cmdSuggest(cfg, args.flags, positional);
|
|
1208
|
+
case 'suggestions': return cmdSuggestions(cfg, args.flags);
|
|
1209
|
+
case 'inbox': return cmdInbox(cfg, args.flags);
|
|
1210
|
+
case 'reply': return cmdReply(cfg, args.flags, positional);
|
|
1211
|
+
case 'post': return cmdPost(cfg, args.flags, positional);
|
|
1212
|
+
case 'note': return cmdNote(cfg, args.flags, positional);
|
|
1213
|
+
case 'feedback': return cmdFeedback(cfg, args.flags, positional);
|
|
1214
|
+
case 'guide': return cmdGuide();
|
|
1215
|
+
case 'spec': return cmdSpec(cfg, args.flags);
|
|
1216
|
+
case 'mcp': return runMcpServer(cfg);
|
|
1217
|
+
case 'version': return console.log(VERSION);
|
|
1218
|
+
case 'help':
|
|
1219
|
+
case undefined: return cmdHelp();
|
|
1220
|
+
default:
|
|
1221
|
+
console.error(`Unknown command: ${command}\nRun \`socialaia help\` for usage.`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
main().catch((err) => {
|
|
1227
|
+
console.error(`❌ ${err.message}`);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
});
|