atris 2.3.4 → 2.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atris/atrisDev.md +716 -0
- package/atris/skills/clawhub/atris/SKILL.md +1 -1
- package/atris/skills/create-member/SKILL.md +248 -0
- package/atris/skills/drive/SKILL.md +38 -2
- package/atris/skills/email-agent/SKILL.md +30 -0
- package/atris/skills/slack/SKILL.md +15 -4
- package/atris/skills/writing/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +144 -0
- package/atris/team/researcher/MEMBER.md +72 -0
- package/bin/atris.js +29 -6
- package/commands/autopilot.js +14 -5
- package/commands/clean.js +1 -1
- package/commands/init.js +11 -5
- package/commands/member.js +461 -0
- package/commands/skill.js +7 -5
- package/commands/sync.js +20 -10
- package/commands/workflow.js +11 -43
- package/lib/file-ops.js +1 -1
- package/lib/journal.js +14 -11
- package/lib/state-detection.js +3 -2
- package/package.json +2 -1
- package/utils/api.js +24 -0
- package/utils/auth.js +7 -4
package/bin/atris.js
CHANGED
|
@@ -219,11 +219,18 @@ function showHelp() {
|
|
|
219
219
|
console.log(' integrations - Show integration status');
|
|
220
220
|
console.log('');
|
|
221
221
|
console.log('Skills:');
|
|
222
|
-
console.log(' skill create <name> - Scaffold a new skill (--integration, --
|
|
222
|
+
console.log(' skill create <name> - Scaffold a new skill (--integration, --local)');
|
|
223
223
|
console.log(' skill link [--all] - Symlink skills to ~/.claude/skills/ (system-level)');
|
|
224
224
|
console.log(' skill list - Show all skills with compliance status');
|
|
225
225
|
console.log(' skill audit [name] - Validate skill against Anthropic guide');
|
|
226
226
|
console.log(' skill fix [name] - Auto-fix common compliance issues');
|
|
227
|
+
console.log(' skill delete <name> - Delete a skill and its symlinks');
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log('Team:');
|
|
230
|
+
console.log(' member create <name> - Scaffold a new team member (MEMBER.md)');
|
|
231
|
+
console.log(' member list - Show all team members');
|
|
232
|
+
console.log(' member activate <n> - Activate a member (link skills, show context)');
|
|
233
|
+
console.log(' member upgrade <n> - Convert flat file to directory format');
|
|
227
234
|
console.log('');
|
|
228
235
|
console.log('Plugin:');
|
|
229
236
|
console.log(' plugin build - Package skills as .plugin for Cowork');
|
|
@@ -336,12 +343,13 @@ const { analyticsAtris: analyticsCmd } = require('../commands/analytics');
|
|
|
336
343
|
const { cleanAtris: cleanCmd } = require('../commands/clean');
|
|
337
344
|
const { verifyAtris: verifyCmd } = require('../commands/verify');
|
|
338
345
|
const { skillCommand: skillCmd } = require('../commands/skill');
|
|
346
|
+
const { memberCommand: memberCmd } = require('../commands/member');
|
|
339
347
|
const { pluginCommand: pluginCmd } = require('../commands/plugin');
|
|
340
348
|
|
|
341
349
|
// Check if this is a known command or natural language input
|
|
342
350
|
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'plan', 'do', 'review',
|
|
343
351
|
'activate', 'agent', 'chat', 'login', 'logout', 'whoami', 'switch', 'accounts', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
344
|
-
'clean', 'verify', 'search', 'skill', 'plugin',
|
|
352
|
+
'clean', 'verify', 'search', 'skill', 'member', 'plugin',
|
|
345
353
|
'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
|
|
346
354
|
|
|
347
355
|
// Check if command is an atris.md spec file - triggers welcome visualization
|
|
@@ -359,6 +367,12 @@ if (isSpecFile(command)) {
|
|
|
359
367
|
if (!command || !knownCommands.includes(command)) {
|
|
360
368
|
const userInput = process.argv.slice(2).join(' ');
|
|
361
369
|
|
|
370
|
+
// Warn if this looks like a mistyped single-word command (no spaces)
|
|
371
|
+
if (command && !userInput.includes(' ')) {
|
|
372
|
+
console.log(`⚠ Unknown command: "${command}". Run "atris help" for available commands.`);
|
|
373
|
+
console.log(' Treating as natural language input...\n');
|
|
374
|
+
}
|
|
375
|
+
|
|
362
376
|
// Launch interactive entry (the "Performance")
|
|
363
377
|
interactiveEntry(userInput)
|
|
364
378
|
.then(() => process.exit(0))
|
|
@@ -827,6 +841,10 @@ if (command === 'init') {
|
|
|
827
841
|
const subcommand = process.argv[3];
|
|
828
842
|
const args = process.argv.slice(4);
|
|
829
843
|
skillCmd(subcommand, ...args);
|
|
844
|
+
} else if (command === 'member') {
|
|
845
|
+
const subcommand = process.argv[3];
|
|
846
|
+
const args = process.argv.slice(4);
|
|
847
|
+
memberCmd(subcommand, ...args);
|
|
830
848
|
} else if (command === 'plugin') {
|
|
831
849
|
const subcommand = process.argv[3] || 'build';
|
|
832
850
|
const args = process.argv.slice(4);
|
|
@@ -837,6 +855,10 @@ if (command === 'init') {
|
|
|
837
855
|
process.exit(1);
|
|
838
856
|
}
|
|
839
857
|
|
|
858
|
+
// NOTE: initAtris, syncAtris, logAtris, appendLog, logSyncAtris, showTodayLog, showRecentLogs
|
|
859
|
+
// are legacy inline implementations. Routing now uses require('../commands/...') instead.
|
|
860
|
+
// The journal utilities (getLogPath, ensureLogDirectory, createLogFile) are still used by
|
|
861
|
+
// top-level code at lines ~393 and ~2553 via hoisting — do not remove without migrating those.
|
|
840
862
|
function initAtris() {
|
|
841
863
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
842
864
|
const teamDir = path.join(targetDir, 'team');
|
|
@@ -900,7 +922,7 @@ function initAtris() {
|
|
|
900
922
|
}
|
|
901
923
|
|
|
902
924
|
// Copy agent templates from package (MEMBER.md directory format)
|
|
903
|
-
const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer'];
|
|
925
|
+
const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer', 'researcher'];
|
|
904
926
|
members.forEach(name => {
|
|
905
927
|
const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
|
|
906
928
|
const memberDir = path.join(teamDir, name);
|
|
@@ -985,6 +1007,7 @@ function syncAtris() {
|
|
|
985
1007
|
{ source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
|
|
986
1008
|
{ source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
|
|
987
1009
|
{ source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
|
|
1010
|
+
{ source: 'atris/team/researcher/MEMBER.md', target: 'team/researcher/MEMBER.md' },
|
|
988
1011
|
{ source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
|
|
989
1012
|
];
|
|
990
1013
|
|
|
@@ -1250,7 +1273,7 @@ async function logSyncAtris() {
|
|
|
1250
1273
|
console.log(`Created local log template for ${dateFormatted}. Fill it in before syncing.`);
|
|
1251
1274
|
}
|
|
1252
1275
|
|
|
1253
|
-
|
|
1276
|
+
let localContent = fs.readFileSync(logFile, 'utf8');
|
|
1254
1277
|
const localHash = computeContentHash(localContent);
|
|
1255
1278
|
|
|
1256
1279
|
// Ensure agent selected
|
|
@@ -2425,7 +2448,7 @@ async function chatOnce(config, credentials, message) {
|
|
|
2425
2448
|
console.log('');
|
|
2426
2449
|
|
|
2427
2450
|
const agentId = config.agent_id;
|
|
2428
|
-
const apiUrl =
|
|
2451
|
+
const apiUrl = getApiBaseUrl().replace(/\/api$/, '');
|
|
2429
2452
|
const endpoint = `${apiUrl}/api/agent/${agentId}/pro-chat`;
|
|
2430
2453
|
|
|
2431
2454
|
const body = JSON.stringify({
|
|
@@ -2482,7 +2505,7 @@ async function chatInteractive(config, credentials) {
|
|
|
2482
2505
|
|
|
2483
2506
|
// Send to pro-chat
|
|
2484
2507
|
console.log('');
|
|
2485
|
-
const apiUrl =
|
|
2508
|
+
const apiUrl = getApiBaseUrl().replace(/\/api$/, '');
|
|
2486
2509
|
const endpoint = `${apiUrl}/api/agent/${agentId}/pro-chat`;
|
|
2487
2510
|
|
|
2488
2511
|
const body = JSON.stringify({
|
package/commands/autopilot.js
CHANGED
|
@@ -122,7 +122,7 @@ async function executePhase(phase, prd, options = {}) {
|
|
|
122
122
|
fs.writeFileSync(tmpFile, prompt);
|
|
123
123
|
|
|
124
124
|
try {
|
|
125
|
-
const cmd = `claude -p "$(cat ${tmpFile})" --allowedTools "Bash,Read,Write,Edit,Glob,Grep"`;
|
|
125
|
+
const cmd = `claude -p "$(cat '${tmpFile.replace(/'/g, "'\\''")}')" --allowedTools "Bash,Read,Write,Edit,Glob,Grep"`;
|
|
126
126
|
const output = execSync(cmd, {
|
|
127
127
|
cwd: process.cwd(),
|
|
128
128
|
encoding: 'utf8',
|
|
@@ -134,7 +134,7 @@ async function executePhase(phase, prd, options = {}) {
|
|
|
134
134
|
// Clean up
|
|
135
135
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
136
136
|
|
|
137
|
-
const result =
|
|
137
|
+
const result = output || '';
|
|
138
138
|
|
|
139
139
|
if (phase === 'plan') {
|
|
140
140
|
console.log('✓ Planning complete');
|
|
@@ -308,6 +308,10 @@ async function autopilotAtris(description, options = {}) {
|
|
|
308
308
|
console.log('✓ Logged to journal');
|
|
309
309
|
console.log('');
|
|
310
310
|
|
|
311
|
+
// Clean up temp files on success
|
|
312
|
+
try { fs.unlinkSync(path.join(process.cwd(), 'prd.json')); } catch {}
|
|
313
|
+
try { fs.unlinkSync(path.join(process.cwd(), 'progress.txt')); } catch {}
|
|
314
|
+
|
|
311
315
|
return { success: true, iterations: iteration };
|
|
312
316
|
}
|
|
313
317
|
|
|
@@ -360,7 +364,11 @@ async function autopilotFromTodo(options = {}) {
|
|
|
360
364
|
return;
|
|
361
365
|
}
|
|
362
366
|
|
|
363
|
-
|
|
367
|
+
// Support both formats: "- [ ] Task" (checkbox) and "- **T1:** Task" (Atris standard)
|
|
368
|
+
const backlogLines = backlogMatch[1].split('\n').filter(line => {
|
|
369
|
+
const trimmed = line.trim();
|
|
370
|
+
return trimmed.startsWith('- [ ]') || trimmed.match(/^- \*\*T\d+:\*\*\s/);
|
|
371
|
+
});
|
|
364
372
|
|
|
365
373
|
if (backlogLines.length === 0) {
|
|
366
374
|
console.log('No unchecked items in backlog. TODO.md is at target state (0 tasks).');
|
|
@@ -368,8 +376,9 @@ async function autopilotFromTodo(options = {}) {
|
|
|
368
376
|
}
|
|
369
377
|
|
|
370
378
|
// Pick first item
|
|
371
|
-
const firstItem = backlogLines[0];
|
|
372
|
-
|
|
379
|
+
const firstItem = backlogLines[0].trim();
|
|
380
|
+
// Try checkbox format first, then Atris standard format
|
|
381
|
+
const itemMatch = firstItem.match(/- \[ \] (.+)/) || firstItem.match(/- \*\*T\d+:\*\*\s*(.+)/);
|
|
373
382
|
|
|
374
383
|
if (!itemMatch) {
|
|
375
384
|
throw new Error('Could not parse backlog item');
|
package/commands/clean.js
CHANGED
|
@@ -240,7 +240,7 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
240
240
|
// Apply replacements
|
|
241
241
|
if (!dryRun && replacements.length > 0) {
|
|
242
242
|
for (const r of replacements) {
|
|
243
|
-
mapContent = mapContent.
|
|
243
|
+
mapContent = mapContent.split(r.old).join(r.new);
|
|
244
244
|
}
|
|
245
245
|
fs.writeFileSync(mapFile, mapContent);
|
|
246
246
|
}
|
package/commands/init.js
CHANGED
|
@@ -157,9 +157,9 @@ function detectProjectContext(projectRoot = process.cwd()) {
|
|
|
157
157
|
* @param {Object} profile - Project profile from detectProjectContext()
|
|
158
158
|
*/
|
|
159
159
|
function injectProjectPatterns(agentTeamDir, profile) {
|
|
160
|
-
const executorFile = path.join(agentTeamDir, 'executor.md');
|
|
161
|
-
const navigatorFile = path.join(agentTeamDir, 'navigator.md');
|
|
162
|
-
const validatorFile = path.join(agentTeamDir, 'validator.md');
|
|
160
|
+
const executorFile = path.join(agentTeamDir, 'executor', 'MEMBER.md');
|
|
161
|
+
const navigatorFile = path.join(agentTeamDir, 'navigator', 'MEMBER.md');
|
|
162
|
+
const validatorFile = path.join(agentTeamDir, 'validator', 'MEMBER.md');
|
|
163
163
|
|
|
164
164
|
// Inject into executor.md
|
|
165
165
|
if (fs.existsSync(executorFile)) {
|
|
@@ -798,8 +798,14 @@ After displaying the boot output, respond to the user naturally.
|
|
|
798
798
|
if (content.includes(startMarker)) {
|
|
799
799
|
// Replace existing Atris block
|
|
800
800
|
const startIdx = content.indexOf(startMarker);
|
|
801
|
-
const
|
|
802
|
-
|
|
801
|
+
const endRaw = content.indexOf(endMarker);
|
|
802
|
+
if (endRaw === -1) {
|
|
803
|
+
// End marker missing — replace from start marker to end with fresh block
|
|
804
|
+
content = atrisBlock + content.slice(0, startIdx);
|
|
805
|
+
} else {
|
|
806
|
+
const endIdx = endRaw + endMarker.length;
|
|
807
|
+
content = atrisBlock + content.slice(0, startIdx) + content.slice(endIdx).replace(/^\n+/, '');
|
|
808
|
+
}
|
|
803
809
|
fs.writeFileSync(rootClaudeMd, content);
|
|
804
810
|
console.log('✓ Updated Atris block in CLAUDE.md');
|
|
805
811
|
} else {
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// --- YAML Frontmatter Parser (shared with skill.js) ---
|
|
5
|
+
|
|
6
|
+
function parseFrontmatter(content) {
|
|
7
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
|
|
10
|
+
const yaml = match[1];
|
|
11
|
+
const result = {};
|
|
12
|
+
let currentKey = null;
|
|
13
|
+
|
|
14
|
+
for (const line of yaml.split('\n')) {
|
|
15
|
+
const listMatch = line.match(/^\s+-\s+(.+)$/);
|
|
16
|
+
if (listMatch && currentKey) {
|
|
17
|
+
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
18
|
+
result[currentKey].push(listMatch[1].trim());
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nestedMatch = line.match(/^\s+([a-z_-]+):\s*(.*)$/);
|
|
23
|
+
if (nestedMatch && currentKey && typeof result[currentKey] === 'object' && !Array.isArray(result[currentKey])) {
|
|
24
|
+
const val = nestedMatch[2].trim();
|
|
25
|
+
result[currentKey][nestedMatch[1]] = val === 'true' ? true : val === 'false' ? false : val || true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const kvMatch = line.match(/^([a-z_-]+):\s*(.*)$/);
|
|
30
|
+
if (kvMatch) {
|
|
31
|
+
currentKey = kvMatch[1];
|
|
32
|
+
const val = kvMatch[2].trim();
|
|
33
|
+
if (val === '') {
|
|
34
|
+
result[currentKey] = {};
|
|
35
|
+
} else if (val.startsWith('[') && val.endsWith(']')) {
|
|
36
|
+
result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
37
|
+
} else {
|
|
38
|
+
result[currentKey] = val.replace(/^["']|["']$/g, '');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Member Discovery ---
|
|
47
|
+
|
|
48
|
+
function findAllMembers(teamDir) {
|
|
49
|
+
if (!fs.existsSync(teamDir)) return [];
|
|
50
|
+
|
|
51
|
+
const members = [];
|
|
52
|
+
const entries = fs.readdirSync(teamDir);
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const fullPath = path.join(teamDir, entry);
|
|
56
|
+
const stat = fs.statSync(fullPath);
|
|
57
|
+
|
|
58
|
+
// Directory format: team/<name>/MEMBER.md
|
|
59
|
+
if (stat.isDirectory()) {
|
|
60
|
+
const memberFile = path.join(fullPath, 'MEMBER.md');
|
|
61
|
+
if (fs.existsSync(memberFile)) {
|
|
62
|
+
const content = fs.readFileSync(memberFile, 'utf8');
|
|
63
|
+
const fm = parseFrontmatter(content) || {};
|
|
64
|
+
|
|
65
|
+
// Count local skills
|
|
66
|
+
const skillsDir = path.join(fullPath, 'skills');
|
|
67
|
+
let skillCount = 0;
|
|
68
|
+
if (fs.existsSync(skillsDir)) {
|
|
69
|
+
const skillEntries = fs.readdirSync(skillsDir);
|
|
70
|
+
for (const s of skillEntries) {
|
|
71
|
+
if (fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))) skillCount++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Count context files
|
|
76
|
+
const contextDir = path.join(fullPath, 'context');
|
|
77
|
+
let contextCount = 0;
|
|
78
|
+
if (fs.existsSync(contextDir)) {
|
|
79
|
+
contextCount = fs.readdirSync(contextDir).filter(f => f.endsWith('.md')).length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for tools
|
|
83
|
+
const toolsDir = path.join(fullPath, 'tools');
|
|
84
|
+
const hasTools = fs.existsSync(toolsDir);
|
|
85
|
+
|
|
86
|
+
members.push({
|
|
87
|
+
name: fm.name || entry,
|
|
88
|
+
role: fm.role || '(no role)',
|
|
89
|
+
description: fm.description || '',
|
|
90
|
+
version: fm.version || '',
|
|
91
|
+
format: 'directory',
|
|
92
|
+
path: memberFile,
|
|
93
|
+
dir: fullPath,
|
|
94
|
+
skillCount,
|
|
95
|
+
contextCount,
|
|
96
|
+
hasTools,
|
|
97
|
+
skills: Array.isArray(fm.skills) ? fm.skills : [],
|
|
98
|
+
permissions: fm.permissions || {},
|
|
99
|
+
frontmatter: fm
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Flat file format: team/<name>.md
|
|
106
|
+
if (entry.endsWith('.md') && stat.isFile()) {
|
|
107
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
108
|
+
const fm = parseFrontmatter(content);
|
|
109
|
+
if (!fm) continue; // No frontmatter = not a member
|
|
110
|
+
|
|
111
|
+
const name = entry.replace('.md', '');
|
|
112
|
+
members.push({
|
|
113
|
+
name: fm.name || name,
|
|
114
|
+
role: fm.role || '(no role)',
|
|
115
|
+
description: fm.description || '',
|
|
116
|
+
version: fm.version || '',
|
|
117
|
+
format: 'flat',
|
|
118
|
+
path: fullPath,
|
|
119
|
+
dir: path.dirname(fullPath),
|
|
120
|
+
skillCount: 0,
|
|
121
|
+
contextCount: 0,
|
|
122
|
+
hasTools: false,
|
|
123
|
+
skills: Array.isArray(fm.skills) ? fm.skills : [],
|
|
124
|
+
permissions: fm.permissions || {},
|
|
125
|
+
frontmatter: fm
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return members;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- LIST subcommand ---
|
|
134
|
+
|
|
135
|
+
function memberList() {
|
|
136
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
137
|
+
const members = findAllMembers(teamDir);
|
|
138
|
+
|
|
139
|
+
if (members.length === 0) {
|
|
140
|
+
console.log('No team members found in atris/team/.');
|
|
141
|
+
console.log('Run "atris member create <name>" to create one.');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log('Team Members');
|
|
147
|
+
console.log('─'.repeat(70));
|
|
148
|
+
|
|
149
|
+
const nameW = 16;
|
|
150
|
+
const roleW = 16;
|
|
151
|
+
const fmtW = 6;
|
|
152
|
+
const skillW = 8;
|
|
153
|
+
const ctxW = 8;
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
' ' +
|
|
157
|
+
'Name'.padEnd(nameW) +
|
|
158
|
+
'Role'.padEnd(roleW) +
|
|
159
|
+
'Format'.padEnd(fmtW) +
|
|
160
|
+
'Skills'.padEnd(skillW) +
|
|
161
|
+
'Context'.padEnd(ctxW) +
|
|
162
|
+
'Version'
|
|
163
|
+
);
|
|
164
|
+
console.log(' ' + '─'.repeat(66));
|
|
165
|
+
|
|
166
|
+
for (const m of members) {
|
|
167
|
+
const skills = m.format === 'directory' ? String(m.skillCount) : '-';
|
|
168
|
+
const context = m.format === 'directory' ? String(m.contextCount) : '-';
|
|
169
|
+
console.log(
|
|
170
|
+
' ' +
|
|
171
|
+
m.name.padEnd(nameW) +
|
|
172
|
+
m.role.substring(0, roleW - 1).padEnd(roleW) +
|
|
173
|
+
(m.format === 'directory' ? 'dir' : 'flat').padEnd(fmtW) +
|
|
174
|
+
skills.padEnd(skillW) +
|
|
175
|
+
context.padEnd(ctxW) +
|
|
176
|
+
(m.version || '-')
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(`${members.length} member(s) found.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- CREATE subcommand ---
|
|
185
|
+
|
|
186
|
+
function memberCreate(name, ...flags) {
|
|
187
|
+
if (!name) {
|
|
188
|
+
console.error('Usage: atris member create <name> [--role="Title"]');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Parse flags
|
|
193
|
+
let role = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');
|
|
194
|
+
let description = '';
|
|
195
|
+
|
|
196
|
+
for (const flag of flags) {
|
|
197
|
+
const roleMatch = flag.match(/^--role=["']?(.+?)["']?$/);
|
|
198
|
+
if (roleMatch) role = roleMatch[1];
|
|
199
|
+
|
|
200
|
+
const descMatch = flag.match(/^--description=["']?(.+?)["']?$/);
|
|
201
|
+
if (descMatch) description = descMatch[1];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
205
|
+
const memberDir = path.join(teamDir, name);
|
|
206
|
+
const memberFile = path.join(memberDir, 'MEMBER.md');
|
|
207
|
+
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
208
|
+
|
|
209
|
+
// Check for existing
|
|
210
|
+
if (fs.existsSync(memberFile)) {
|
|
211
|
+
console.error(`Member "${name}" already exists at team/${name}/MEMBER.md`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (fs.existsSync(legacyFile)) {
|
|
215
|
+
console.error(`Member "${name}" already exists at team/${name}.md`);
|
|
216
|
+
console.log(`Run "atris member upgrade ${name}" to convert to directory format.`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scaffold
|
|
221
|
+
fs.mkdirSync(memberDir, { recursive: true });
|
|
222
|
+
fs.mkdirSync(path.join(memberDir, 'skills'), { recursive: true });
|
|
223
|
+
fs.mkdirSync(path.join(memberDir, 'tools'), { recursive: true });
|
|
224
|
+
fs.mkdirSync(path.join(memberDir, 'context'), { recursive: true });
|
|
225
|
+
|
|
226
|
+
const content = `---
|
|
227
|
+
name: ${name}
|
|
228
|
+
role: ${role}
|
|
229
|
+
description: ${description || `Handles ${role.toLowerCase()} tasks`}
|
|
230
|
+
version: 1.0.0
|
|
231
|
+
|
|
232
|
+
skills: []
|
|
233
|
+
|
|
234
|
+
permissions:
|
|
235
|
+
can-read: true
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
# ${role}
|
|
239
|
+
|
|
240
|
+
## Persona
|
|
241
|
+
|
|
242
|
+
(Define how this member communicates, their tone, and decision-making style)
|
|
243
|
+
|
|
244
|
+
## Workflow
|
|
245
|
+
|
|
246
|
+
1. Step one
|
|
247
|
+
2. Step two
|
|
248
|
+
3. Step three
|
|
249
|
+
|
|
250
|
+
## Rules
|
|
251
|
+
|
|
252
|
+
1. Rule one
|
|
253
|
+
2. Rule two
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
fs.writeFileSync(memberFile, content);
|
|
257
|
+
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(`✓ Created team/${name}/MEMBER.md`);
|
|
260
|
+
console.log(`✓ Created team/${name}/skills/`);
|
|
261
|
+
console.log(`✓ Created team/${name}/tools/`);
|
|
262
|
+
console.log(`✓ Created team/${name}/context/`);
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log(`Next: edit team/${name}/MEMBER.md to define persona, workflow, and permissions.`);
|
|
265
|
+
console.log(` add skills to team/${name}/skills/<skill-name>/SKILL.md`);
|
|
266
|
+
console.log(` add context docs to team/${name}/context/`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- ACTIVATE subcommand ---
|
|
270
|
+
|
|
271
|
+
function memberActivate(name) {
|
|
272
|
+
if (!name) {
|
|
273
|
+
console.error('Usage: atris member activate <name>');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
278
|
+
const memberDir = path.join(teamDir, name);
|
|
279
|
+
const memberFile = path.join(memberDir, 'MEMBER.md');
|
|
280
|
+
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
281
|
+
|
|
282
|
+
// Find the member (directory first, flat fallback)
|
|
283
|
+
let activePath = null;
|
|
284
|
+
let activeDir = null;
|
|
285
|
+
let isLegacy = false;
|
|
286
|
+
|
|
287
|
+
if (fs.existsSync(memberFile)) {
|
|
288
|
+
activePath = memberFile;
|
|
289
|
+
activeDir = memberDir;
|
|
290
|
+
} else if (fs.existsSync(legacyFile)) {
|
|
291
|
+
activePath = legacyFile;
|
|
292
|
+
activeDir = teamDir;
|
|
293
|
+
isLegacy = true;
|
|
294
|
+
} else {
|
|
295
|
+
console.error(`Member "${name}" not found. Run "atris member list".`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const content = fs.readFileSync(activePath, 'utf8');
|
|
300
|
+
const fm = parseFrontmatter(content) || {};
|
|
301
|
+
|
|
302
|
+
console.log('');
|
|
303
|
+
console.log(`Activating: ${fm.name || name} (${fm.role || 'no role'})`);
|
|
304
|
+
|
|
305
|
+
// If legacy format, offer upgrade
|
|
306
|
+
if (isLegacy) {
|
|
307
|
+
console.log(` Format: flat file (team/${name}.md)`);
|
|
308
|
+
console.log(` Tip: run "atris member upgrade ${name}" to convert to directory format.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Symlink member's local skills to system-level
|
|
312
|
+
if (!isLegacy) {
|
|
313
|
+
const skillsDir = path.join(activeDir, 'skills');
|
|
314
|
+
if (fs.existsSync(skillsDir)) {
|
|
315
|
+
const home = require('os').homedir();
|
|
316
|
+
const toolDirs = [
|
|
317
|
+
{ dir: path.join(home, '.claude', 'skills'), label: 'Claude' },
|
|
318
|
+
{ dir: path.join(home, '.codex', 'skills'), label: 'Codex' },
|
|
319
|
+
{ dir: path.join(home, '.cursor', 'skills'), label: 'Cursor' },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
for (const { dir } of toolDirs) {
|
|
323
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const skillEntries = fs.readdirSync(skillsDir);
|
|
327
|
+
let linked = 0;
|
|
328
|
+
|
|
329
|
+
for (const entry of skillEntries) {
|
|
330
|
+
const skillDir = path.join(skillsDir, entry);
|
|
331
|
+
if (!fs.statSync(skillDir).isDirectory()) continue;
|
|
332
|
+
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) continue;
|
|
333
|
+
|
|
334
|
+
for (const { dir, label } of toolDirs) {
|
|
335
|
+
const linkPath = path.join(dir, entry);
|
|
336
|
+
if (fs.existsSync(linkPath)) continue;
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
fs.symlinkSync(skillDir, linkPath);
|
|
340
|
+
} catch (e) { /* silent */ }
|
|
341
|
+
}
|
|
342
|
+
linked++;
|
|
343
|
+
console.log(` ✓ Linked skill: ${entry}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (linked === 0) {
|
|
347
|
+
console.log(' No local skills to link.');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Show context files
|
|
352
|
+
const contextDir = path.join(activeDir, 'context');
|
|
353
|
+
if (fs.existsSync(contextDir)) {
|
|
354
|
+
const ctxFiles = fs.readdirSync(contextDir).filter(f => f.endsWith('.md'));
|
|
355
|
+
if (ctxFiles.length > 0) {
|
|
356
|
+
console.log(` Context: ${ctxFiles.join(', ')}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Show tools
|
|
361
|
+
const toolsDir = path.join(activeDir, 'tools');
|
|
362
|
+
if (fs.existsSync(toolsDir)) {
|
|
363
|
+
const toolFiles = fs.readdirSync(toolsDir);
|
|
364
|
+
if (toolFiles.length > 0) {
|
|
365
|
+
console.log(` Tools: ${toolFiles.join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Show permissions
|
|
371
|
+
if (fm.permissions && typeof fm.permissions === 'object') {
|
|
372
|
+
const perms = Object.entries(fm.permissions);
|
|
373
|
+
if (perms.length > 0) {
|
|
374
|
+
const allowed = perms.filter(([, v]) => v === true || v === 'true').map(([k]) => k);
|
|
375
|
+
const denied = perms.filter(([, v]) => v === false || v === 'false').map(([k]) => k);
|
|
376
|
+
if (allowed.length) console.log(` Allowed: ${allowed.join(', ')}`);
|
|
377
|
+
if (denied.length) console.log(` Denied: ${denied.join(', ')}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log(`Member "${fm.name || name}" activated.`);
|
|
383
|
+
console.log(`Tell your agent: "You are the ${fm.role || name}. Read team/${name}/MEMBER.md."`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- UPGRADE subcommand ---
|
|
387
|
+
|
|
388
|
+
function memberUpgrade(name) {
|
|
389
|
+
if (!name) {
|
|
390
|
+
console.error('Usage: atris member upgrade <name>');
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
395
|
+
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
396
|
+
const memberDir = path.join(teamDir, name);
|
|
397
|
+
const memberFile = path.join(memberDir, 'MEMBER.md');
|
|
398
|
+
|
|
399
|
+
if (!fs.existsSync(legacyFile)) {
|
|
400
|
+
if (fs.existsSync(memberFile)) {
|
|
401
|
+
console.log(`"${name}" is already in directory format.`);
|
|
402
|
+
} else {
|
|
403
|
+
console.error(`Member "${name}" not found at team/${name}.md`);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (fs.existsSync(memberDir)) {
|
|
409
|
+
console.error(`Directory team/${name}/ already exists. Resolve manually.`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Move flat file to directory format
|
|
414
|
+
fs.mkdirSync(memberDir, { recursive: true });
|
|
415
|
+
fs.mkdirSync(path.join(memberDir, 'skills'), { recursive: true });
|
|
416
|
+
fs.mkdirSync(path.join(memberDir, 'tools'), { recursive: true });
|
|
417
|
+
fs.mkdirSync(path.join(memberDir, 'context'), { recursive: true });
|
|
418
|
+
fs.renameSync(legacyFile, memberFile);
|
|
419
|
+
|
|
420
|
+
console.log(`✓ Upgraded team/${name}.md → team/${name}/MEMBER.md`);
|
|
421
|
+
console.log(`✓ Created skills/, tools/, context/ directories`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --- Command Dispatcher ---
|
|
425
|
+
|
|
426
|
+
function memberCommand(subcommand, ...args) {
|
|
427
|
+
switch (subcommand) {
|
|
428
|
+
case 'list':
|
|
429
|
+
case 'ls':
|
|
430
|
+
return memberList();
|
|
431
|
+
case 'create':
|
|
432
|
+
case 'new':
|
|
433
|
+
return memberCreate(args[0], ...args.slice(1));
|
|
434
|
+
case 'activate':
|
|
435
|
+
return memberActivate(args[0]);
|
|
436
|
+
case 'upgrade':
|
|
437
|
+
return memberUpgrade(args[0]);
|
|
438
|
+
default:
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log('Usage: atris member <subcommand> [name]');
|
|
441
|
+
console.log('');
|
|
442
|
+
console.log('Subcommands:');
|
|
443
|
+
console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs)');
|
|
444
|
+
console.log(' list Show all team members');
|
|
445
|
+
console.log(' activate <name> Symlink member skills, show context and permissions');
|
|
446
|
+
console.log(' upgrade <name> Convert flat file (name.md) to directory format');
|
|
447
|
+
console.log('');
|
|
448
|
+
console.log('Create flags:');
|
|
449
|
+
console.log(' --role="Title" Set the member role');
|
|
450
|
+
console.log(' --description="..." Set the member description');
|
|
451
|
+
console.log('');
|
|
452
|
+
console.log('Examples:');
|
|
453
|
+
console.log(' atris member create sdr --role="Sales Development Rep"');
|
|
454
|
+
console.log(' atris member list');
|
|
455
|
+
console.log(' atris member activate navigator');
|
|
456
|
+
console.log(' atris member upgrade executor');
|
|
457
|
+
console.log('');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = { memberCommand, findAllMembers, parseFrontmatter };
|