@tryfridayai/cli 0.2.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/bin/friday.js +13 -0
- package/bin/postinstall.js +27 -0
- package/package.json +47 -0
- package/src/cli.js +132 -0
- package/src/commands/chat/slashCommands.js +976 -0
- package/src/commands/chat/smartAffordances.js +139 -0
- package/src/commands/chat/ui.js +146 -0
- package/src/commands/chat/welcomeScreen.js +161 -0
- package/src/commands/chat.js +763 -0
- package/src/commands/install.js +152 -0
- package/src/commands/plugins.js +65 -0
- package/src/commands/schedule.js +278 -0
- package/src/commands/serve.js +206 -0
- package/src/commands/setup.js +187 -0
- package/src/commands/uninstall.js +76 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* friday install <plugin> — Install a plugin with guided credential setup
|
|
3
|
+
*
|
|
4
|
+
* Reads the plugin manifest from the catalog, prompts for required
|
|
5
|
+
* credentials, stores them, and registers the plugin as installed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
let runtimeDir;
|
|
14
|
+
try {
|
|
15
|
+
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
16
|
+
runtimeDir = path.dirname(runtimePkg);
|
|
17
|
+
} catch {
|
|
18
|
+
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DIM = '\x1b[2m';
|
|
22
|
+
const RESET = '\x1b[0m';
|
|
23
|
+
const BOLD = '\x1b[1m';
|
|
24
|
+
const GREEN = '\x1b[32m';
|
|
25
|
+
const RED = '\x1b[31m';
|
|
26
|
+
const YELLOW = '\x1b[33m';
|
|
27
|
+
|
|
28
|
+
function ask(rl, question) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function maskValue(value) {
|
|
35
|
+
if (!value || value.length < 8) return '****';
|
|
36
|
+
return value.slice(0, 4) + '...' + value.slice(-4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function install(args) {
|
|
40
|
+
const pluginId = args._[1]; // friday install <plugin>
|
|
41
|
+
|
|
42
|
+
// Lazy-import the PluginManager from the runtime package
|
|
43
|
+
const { PluginManager } = await import(path.join(runtimeDir, 'src', 'plugins', 'PluginManager.js'));
|
|
44
|
+
const pm = new PluginManager();
|
|
45
|
+
|
|
46
|
+
// No plugin specified — show available plugins
|
|
47
|
+
if (!pluginId) {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(` ${BOLD}Usage:${RESET} friday install <plugin>`);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(` ${BOLD}Available plugins:${RESET}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
const available = pm.listAvailable();
|
|
54
|
+
const categories = {};
|
|
55
|
+
for (const p of available) {
|
|
56
|
+
if (!categories[p.category]) categories[p.category] = [];
|
|
57
|
+
categories[p.category].push(p);
|
|
58
|
+
}
|
|
59
|
+
for (const [category, plugins] of Object.entries(categories)) {
|
|
60
|
+
console.log(` ${DIM}${category}${RESET}`);
|
|
61
|
+
for (const p of plugins) {
|
|
62
|
+
const status = p.installed ? `${GREEN}installed${RESET}` : '';
|
|
63
|
+
console.log(` ${BOLD}${p.id}${RESET} ${p.description} ${status}`);
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
console.log(` Example: ${DIM}friday install github${RESET}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if plugin exists in catalog
|
|
73
|
+
const manifest = pm.getPluginManifest(pluginId);
|
|
74
|
+
if (!manifest) {
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(` ${RED}Unknown plugin: ${pluginId}${RESET}`);
|
|
77
|
+
console.log(` Run ${DIM}friday install${RESET} to see available plugins.`);
|
|
78
|
+
console.log('');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if already installed
|
|
83
|
+
if (pm.isInstalled(pluginId)) {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(` ${manifest.name} is already installed.`);
|
|
86
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
87
|
+
const reinstall = await ask(rl, ` Reinstall and update credentials? (y/N): `);
|
|
88
|
+
if (reinstall.toLowerCase() !== 'y') {
|
|
89
|
+
rl.close();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
rl.close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fields = pm.getCredentialFields(pluginId);
|
|
96
|
+
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(` ${BOLD}Installing ${manifest.name}${RESET}`);
|
|
99
|
+
console.log(` ${DIM}${manifest.description}${RESET}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
|
|
102
|
+
// If no credentials needed (e.g. Vercel with remote-oauth)
|
|
103
|
+
if (fields.length === 0) {
|
|
104
|
+
if (manifest.setup?.note) {
|
|
105
|
+
console.log(` ${DIM}${manifest.setup.note}${RESET}`);
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
pm.install(pluginId, {});
|
|
109
|
+
console.log(` ${GREEN}${manifest.name} installed.${RESET}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Prompt for credentials
|
|
115
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
+
const credentials = {};
|
|
117
|
+
|
|
118
|
+
if (manifest.setup?.note) {
|
|
119
|
+
console.log(` ${DIM}${manifest.setup.note}${RESET}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const field of fields) {
|
|
124
|
+
if (field.instructions) {
|
|
125
|
+
console.log(` ${DIM}${field.instructions}${RESET}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const requiredTag = field.required ? '' : ` ${DIM}(optional)${RESET}`;
|
|
129
|
+
const prompt = ` ${field.label}${requiredTag}: `;
|
|
130
|
+
const value = await ask(rl, prompt);
|
|
131
|
+
|
|
132
|
+
if (value) {
|
|
133
|
+
credentials[field.key] = value;
|
|
134
|
+
if (field.type === 'secret') {
|
|
135
|
+
// Clear the line and reprint masked
|
|
136
|
+
process.stdout.write(`\x1b[1A\x1b[2K`);
|
|
137
|
+
console.log(` ${field.label}${requiredTag}: ${DIM}${maskValue(value)}${RESET}`);
|
|
138
|
+
}
|
|
139
|
+
} else if (field.required) {
|
|
140
|
+
console.log(` ${YELLOW}Skipped (required). Plugin may not work without this.${RESET}`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
rl.close();
|
|
146
|
+
|
|
147
|
+
// Install
|
|
148
|
+
pm.install(pluginId, credentials);
|
|
149
|
+
console.log(` ${GREEN}${manifest.name} installed.${RESET}`);
|
|
150
|
+
console.log(` The agent can now use ${manifest.name} tools.`);
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* friday plugins — List installed and available plugins
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
let runtimeDir;
|
|
10
|
+
try {
|
|
11
|
+
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
12
|
+
runtimeDir = path.dirname(runtimePkg);
|
|
13
|
+
} catch {
|
|
14
|
+
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DIM = '\x1b[2m';
|
|
18
|
+
const RESET = '\x1b[0m';
|
|
19
|
+
const BOLD = '\x1b[1m';
|
|
20
|
+
const GREEN = '\x1b[32m';
|
|
21
|
+
|
|
22
|
+
export default async function plugins(args) {
|
|
23
|
+
const { PluginManager } = await import(path.join(runtimeDir, 'src', 'plugins', 'PluginManager.js'));
|
|
24
|
+
const pm = new PluginManager();
|
|
25
|
+
|
|
26
|
+
const installed = pm.listInstalled();
|
|
27
|
+
const available = pm.listAvailable();
|
|
28
|
+
|
|
29
|
+
console.log('');
|
|
30
|
+
|
|
31
|
+
// Show installed plugins
|
|
32
|
+
if (installed.length > 0) {
|
|
33
|
+
console.log(` ${BOLD}Installed:${RESET}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
for (const p of installed) {
|
|
36
|
+
console.log(` ${GREEN}${p.id}${RESET} ${p.description}`);
|
|
37
|
+
}
|
|
38
|
+
console.log('');
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` ${DIM}No plugins installed yet.${RESET}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Show available (not installed) plugins
|
|
45
|
+
const notInstalled = available.filter(p => !p.installed);
|
|
46
|
+
if (notInstalled.length > 0) {
|
|
47
|
+
console.log(` ${BOLD}Available:${RESET}`);
|
|
48
|
+
console.log('');
|
|
49
|
+
const categories = {};
|
|
50
|
+
for (const p of notInstalled) {
|
|
51
|
+
if (!categories[p.category]) categories[p.category] = [];
|
|
52
|
+
categories[p.category].push(p);
|
|
53
|
+
}
|
|
54
|
+
for (const [category, plugins] of Object.entries(categories)) {
|
|
55
|
+
console.log(` ${DIM}${category}${RESET}`);
|
|
56
|
+
for (const p of plugins) {
|
|
57
|
+
console.log(` ${BOLD}${p.id}${RESET} ${p.description}`);
|
|
58
|
+
}
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(` Install with: ${DIM}friday install <plugin>${RESET}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* friday schedule — Manage scheduled agents
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* friday schedule List all scheduled agents
|
|
6
|
+
* friday schedule create Create a new scheduled agent
|
|
7
|
+
* friday schedule delete <id> Delete a scheduled agent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import readline from 'readline';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
let runtimeDir;
|
|
16
|
+
try {
|
|
17
|
+
const runtimePkg = require.resolve('friday-runtime/package.json');
|
|
18
|
+
runtimeDir = path.dirname(runtimePkg);
|
|
19
|
+
} catch {
|
|
20
|
+
runtimeDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'runtime');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DIM = '\x1b[2m';
|
|
24
|
+
const RESET = '\x1b[0m';
|
|
25
|
+
const BOLD = '\x1b[1m';
|
|
26
|
+
const GREEN = '\x1b[32m';
|
|
27
|
+
const RED = '\x1b[31m';
|
|
28
|
+
const YELLOW = '\x1b[33m';
|
|
29
|
+
|
|
30
|
+
function ask(rl, question) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse natural language schedule into cron expression.
|
|
38
|
+
*/
|
|
39
|
+
function parseSchedule(input) {
|
|
40
|
+
const lower = input.toLowerCase().trim();
|
|
41
|
+
|
|
42
|
+
// "every X hours"
|
|
43
|
+
const hoursMatch = lower.match(/every\s+(\d+)\s+hours?/);
|
|
44
|
+
if (hoursMatch) {
|
|
45
|
+
const hours = parseInt(hoursMatch[1]);
|
|
46
|
+
return { cron: `0 */${hours} * * *`, humanReadable: `Every ${hours} hours` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// "every X minutes"
|
|
50
|
+
const minutesMatch = lower.match(/every\s+(\d+)\s+minutes?/);
|
|
51
|
+
if (minutesMatch) {
|
|
52
|
+
const mins = parseInt(minutesMatch[1]);
|
|
53
|
+
return { cron: `*/${mins} * * * *`, humanReadable: `Every ${mins} minutes` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// "every day at Xam/pm" or "daily at X"
|
|
57
|
+
const dailyMatch = lower.match(/(?:every\s+day|daily)\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
58
|
+
if (dailyMatch) {
|
|
59
|
+
let hour = parseInt(dailyMatch[1]);
|
|
60
|
+
const minute = parseInt(dailyMatch[2] || '0');
|
|
61
|
+
const ampm = dailyMatch[3];
|
|
62
|
+
if (ampm === 'pm' && hour < 12) hour += 12;
|
|
63
|
+
if (ampm === 'am' && hour === 12) hour = 0;
|
|
64
|
+
const timeStr = `${hour}:${minute.toString().padStart(2, '0')}`;
|
|
65
|
+
return { cron: `${minute} ${hour} * * *`, humanReadable: `Every day at ${timeStr}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// "every morning" (9am)
|
|
69
|
+
if (lower.includes('every morning')) {
|
|
70
|
+
return { cron: '0 9 * * *', humanReadable: 'Every morning at 9:00' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// "every evening" (6pm)
|
|
74
|
+
if (lower.includes('every evening')) {
|
|
75
|
+
return { cron: '0 18 * * *', humanReadable: 'Every evening at 18:00' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// "every hour"
|
|
79
|
+
if (lower === 'every hour' || lower === 'hourly') {
|
|
80
|
+
return { cron: '0 * * * *', humanReadable: 'Every hour' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Day-of-week patterns: "every monday at 9am"
|
|
84
|
+
const dayMatch = lower.match(/every\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
85
|
+
if (dayMatch) {
|
|
86
|
+
const days = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6 };
|
|
87
|
+
const day = days[dayMatch[1].toLowerCase()];
|
|
88
|
+
let hour = parseInt(dayMatch[2]);
|
|
89
|
+
const minute = parseInt(dayMatch[3] || '0');
|
|
90
|
+
const ampm = dayMatch[4];
|
|
91
|
+
if (ampm === 'pm' && hour < 12) hour += 12;
|
|
92
|
+
if (ampm === 'am' && hour === 12) hour = 0;
|
|
93
|
+
return {
|
|
94
|
+
cron: `${minute} ${hour} * * ${day}`,
|
|
95
|
+
humanReadable: `Every ${dayMatch[1]} at ${hour}:${minute.toString().padStart(2, '0')}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// "weekdays at Xam"
|
|
100
|
+
const weekdayMatch = lower.match(/weekdays?\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
101
|
+
if (weekdayMatch) {
|
|
102
|
+
let hour = parseInt(weekdayMatch[1]);
|
|
103
|
+
const minute = parseInt(weekdayMatch[2] || '0');
|
|
104
|
+
const ampm = weekdayMatch[3];
|
|
105
|
+
if (ampm === 'pm' && hour < 12) hour += 12;
|
|
106
|
+
if (ampm === 'am' && hour === 12) hour = 0;
|
|
107
|
+
return {
|
|
108
|
+
cron: `${minute} ${hour} * * 1-5`,
|
|
109
|
+
humanReadable: `Weekdays at ${hour}:${minute.toString().padStart(2, '0')}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Raw cron expression (5 fields)
|
|
114
|
+
if (/^[\d*/,-]+\s+[\d*/,-]+\s+[\d*/,-]+\s+[\d*/,-]+\s+[\d*/,-]+$/.test(lower)) {
|
|
115
|
+
return { cron: lower, humanReadable: lower };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default async function schedule(args) {
|
|
122
|
+
const subcommand = args._[1]; // friday schedule <subcommand>
|
|
123
|
+
|
|
124
|
+
const ScheduledAgentStore = (await import(path.join(runtimeDir, 'src', 'scheduled-agents', 'ScheduledAgentStore.js'))).default;
|
|
125
|
+
const store = new ScheduledAgentStore();
|
|
126
|
+
|
|
127
|
+
// ── List ────────────────────────────────────────────────────────────
|
|
128
|
+
if (!subcommand || subcommand === 'list') {
|
|
129
|
+
const agents = await store.listAgents('default');
|
|
130
|
+
|
|
131
|
+
console.log('');
|
|
132
|
+
if (agents.length === 0) {
|
|
133
|
+
console.log(` ${DIM}No scheduled agents.${RESET}`);
|
|
134
|
+
console.log(` Create one with: ${DIM}friday schedule create${RESET}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(` ${BOLD}Scheduled agents:${RESET}`);
|
|
137
|
+
console.log('');
|
|
138
|
+
for (const agent of agents) {
|
|
139
|
+
const status = agent.status === 'active' ? `${GREEN}active${RESET}` : `${DIM}${agent.status}${RESET}`;
|
|
140
|
+
const schedule = agent.schedule?.humanReadable || agent.schedule?.cron || 'unknown';
|
|
141
|
+
const nextRun = agent.nextRunAt ? new Date(agent.nextRunAt).toLocaleString() : 'unknown';
|
|
142
|
+
console.log(` ${BOLD}${agent.name}${RESET} ${DIM}(${agent.id})${RESET}`);
|
|
143
|
+
console.log(` ${schedule} · ${status} · Next: ${nextRun}`);
|
|
144
|
+
if (agent.description) {
|
|
145
|
+
console.log(` ${DIM}${agent.description}${RESET}`);
|
|
146
|
+
}
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Create ──────────────────────────────────────────────────────────
|
|
155
|
+
if (subcommand === 'create') {
|
|
156
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
157
|
+
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(` ${BOLD}Create a scheduled agent${RESET}`);
|
|
160
|
+
console.log(` ${DIM}Describe what you want in natural language.${RESET}`);
|
|
161
|
+
console.log(` ${DIM}Examples:${RESET}`);
|
|
162
|
+
console.log(` ${DIM}"check my emails every morning at 9am"${RESET}`);
|
|
163
|
+
console.log(` ${DIM}"research AI news every day at 8am using firecrawl"${RESET}`);
|
|
164
|
+
console.log(` ${DIM}"run tests every 3 hours"${RESET}`);
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
const description = await ask(rl, ' > ');
|
|
168
|
+
if (!description) {
|
|
169
|
+
console.log(` ${RED}Description is required.${RESET}`);
|
|
170
|
+
rl.close();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Parse schedule from the description
|
|
175
|
+
let parsed = parseSchedule(description);
|
|
176
|
+
|
|
177
|
+
// If no schedule detected, ask for it separately
|
|
178
|
+
if (!parsed) {
|
|
179
|
+
console.log(` ${DIM}Couldn't detect a schedule. When should this run?${RESET}`);
|
|
180
|
+
console.log(` ${DIM}Examples: "every morning at 9am", "every 3 hours", "weekdays at 8am"${RESET}`);
|
|
181
|
+
const scheduleInput = await ask(rl, ' Schedule: ');
|
|
182
|
+
parsed = parseSchedule(scheduleInput);
|
|
183
|
+
if (!parsed) {
|
|
184
|
+
console.log(` ${RED}Could not parse schedule: "${scheduleInput}"${RESET}`);
|
|
185
|
+
rl.close();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract a name from the description (first few meaningful words)
|
|
191
|
+
const nameFromDesc = description
|
|
192
|
+
.replace(/every\s+(day|morning|evening|hour|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b.*$/i, '')
|
|
193
|
+
.replace(/\b(at\s+\d{1,2}(:\d{2})?\s*(am|pm)?)\b/gi, '')
|
|
194
|
+
.replace(/\b(daily|hourly|weekdays?)\b/gi, '')
|
|
195
|
+
.replace(/\bevery\s+\d+\s+(minutes?|hours?)\b/gi, '')
|
|
196
|
+
.replace(/\busing\s+\w+\b/gi, '')
|
|
197
|
+
.trim();
|
|
198
|
+
const name = nameFromDesc.length > 3
|
|
199
|
+
? nameFromDesc.charAt(0).toUpperCase() + nameFromDesc.slice(1)
|
|
200
|
+
: description.slice(0, 50);
|
|
201
|
+
|
|
202
|
+
// Extract plugin names if mentioned (e.g., "using firecrawl")
|
|
203
|
+
const pluginMatch = description.match(/\busing\s+([\w,\s]+?)(?:\s+(?:every|daily|hourly|at)\b|$)/i);
|
|
204
|
+
const plugins = pluginMatch
|
|
205
|
+
? pluginMatch[1].split(/[,\s]+/).map(p => p.trim()).filter(Boolean)
|
|
206
|
+
: [];
|
|
207
|
+
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(` ${DIM}Name: ${name}${RESET}`);
|
|
210
|
+
console.log(` ${DIM}Schedule: ${parsed.humanReadable} (${parsed.cron})${RESET}`);
|
|
211
|
+
console.log(` ${DIM}Task: ${description}${RESET}`);
|
|
212
|
+
if (plugins.length > 0) {
|
|
213
|
+
console.log(` ${DIM}Plugins: ${plugins.join(', ')}${RESET}`);
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
|
|
217
|
+
const confirm = await ask(rl, ` Create this agent? (Y/n): `);
|
|
218
|
+
rl.close();
|
|
219
|
+
|
|
220
|
+
if (confirm.toLowerCase() === 'n') {
|
|
221
|
+
console.log(` ${DIM}Cancelled.${RESET}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const agent = await store.createAgent('default', {
|
|
227
|
+
name,
|
|
228
|
+
description: description.slice(0, 100),
|
|
229
|
+
instructions: description,
|
|
230
|
+
schedule: {
|
|
231
|
+
cron: parsed.cron,
|
|
232
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
233
|
+
humanReadable: parsed.humanReadable,
|
|
234
|
+
},
|
|
235
|
+
mcpServers: plugins.length > 0 ? plugins : ['terminal'],
|
|
236
|
+
permissions: { preAuthorized: true, tools: [] },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(` ${GREEN}Agent created: ${agent.name}${RESET} ${DIM}(${agent.id})${RESET}`);
|
|
241
|
+
console.log(` ${parsed.humanReadable}`);
|
|
242
|
+
console.log(` ${DIM}Will take effect next time Friday starts.${RESET}`);
|
|
243
|
+
console.log('');
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.log(` ${RED}Failed to create agent: ${error.message}${RESET}`);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Delete ──────────────────────────────────────────────────────────
|
|
251
|
+
if (subcommand === 'delete') {
|
|
252
|
+
const agentId = args._[2];
|
|
253
|
+
if (!agentId) {
|
|
254
|
+
console.log(` ${BOLD}Usage:${RESET} friday schedule delete <agent-id>`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
259
|
+
const confirm = await ask(rl, ` Delete agent ${agentId}? (y/N): `);
|
|
260
|
+
rl.close();
|
|
261
|
+
|
|
262
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
263
|
+
console.log(` ${DIM}Cancelled.${RESET}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await store.deleteAgent('default', agentId);
|
|
269
|
+
console.log(` ${GREEN}Agent deleted.${RESET}`);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.log(` ${RED}Failed: ${error.message}${RESET}`);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(` ${RED}Unknown subcommand: ${subcommand}${RESET}`);
|
|
277
|
+
console.log(` Usage: friday schedule [list|create|delete]`);
|
|
278
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* friday serve — Start the HTTP/WebSocket server
|
|
3
|
+
*
|
|
4
|
+
* Exposes the agent runtime over WebSocket (for real-time streaming)
|
|
5
|
+
* and HTTP REST (for agent/skill management). Clients connect via
|
|
6
|
+
* ws://host:port/ws for conversations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
import { AgentRuntime, loadBackendConfig, agentManager, skillManager } from 'friday-runtime';
|
|
12
|
+
|
|
13
|
+
// --- HTTP helpers ---
|
|
14
|
+
|
|
15
|
+
function parseBody(req) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let body = '';
|
|
18
|
+
req.on('data', (chunk) => (body += chunk));
|
|
19
|
+
req.on('end', () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(body ? JSON.parse(body) : {});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
reject(e);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
req.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sendJson(res, statusCode, data) {
|
|
31
|
+
res.writeHead(statusCode, {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Access-Control-Allow-Origin': '*',
|
|
34
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
35
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
36
|
+
});
|
|
37
|
+
res.end(JSON.stringify(data));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseUrl(url) {
|
|
41
|
+
const [pathname, queryString] = url.split('?');
|
|
42
|
+
const query = {};
|
|
43
|
+
if (queryString) {
|
|
44
|
+
queryString.split('&').forEach((pair) => {
|
|
45
|
+
const [key, value] = pair.split('=');
|
|
46
|
+
query[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return { pathname, query };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Server ---
|
|
53
|
+
|
|
54
|
+
export default async function serve(args) {
|
|
55
|
+
const port = Number(args.port || process.env.PORT || 8787);
|
|
56
|
+
|
|
57
|
+
if (args.help) {
|
|
58
|
+
console.log(`
|
|
59
|
+
friday serve — Start HTTP/WebSocket server
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--port <port> Port to listen on (default: 8787)
|
|
63
|
+
--workspace <path> Working directory for agent (default: ~/FridayWorkspace)
|
|
64
|
+
`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (args.workspace) {
|
|
69
|
+
process.env.FRIDAY_WORKSPACE = args.workspace;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = await loadBackendConfig();
|
|
73
|
+
|
|
74
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
75
|
+
const { pathname, query } = parseUrl(req.url);
|
|
76
|
+
const method = req.method;
|
|
77
|
+
|
|
78
|
+
if (method === 'OPTIONS') {
|
|
79
|
+
res.writeHead(204, {
|
|
80
|
+
'Access-Control-Allow-Origin': '*',
|
|
81
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
82
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
83
|
+
});
|
|
84
|
+
res.end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Health check
|
|
90
|
+
if (pathname === '/health') {
|
|
91
|
+
sendJson(res, 200, { ok: true, workspace: config.workspacePath });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Agent endpoints ---
|
|
96
|
+
|
|
97
|
+
if (pathname === '/api/agents' && method === 'GET') {
|
|
98
|
+
const userId = query.userId || 'default';
|
|
99
|
+
const agents = await agentManager.getUserAgents(userId);
|
|
100
|
+
sendJson(res, 200, { agents });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const agentMatch = pathname.match(/^\/api\/agents\/([^/]+)$/);
|
|
105
|
+
if (agentMatch && method === 'GET') {
|
|
106
|
+
const userId = query.userId || 'default';
|
|
107
|
+
try {
|
|
108
|
+
const agent = await agentManager.loadUserAgentConfig(userId, agentMatch[1]);
|
|
109
|
+
sendJson(res, 200, { agent });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
sendJson(res, 404, { error: error.message });
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (pathname === '/api/agents/custom' && method === 'POST') {
|
|
117
|
+
const body = await parseBody(req);
|
|
118
|
+
const { userId = 'default', agentData } = body;
|
|
119
|
+
const newAgent = await agentManager.createUserAgent(userId, agentData);
|
|
120
|
+
sendJson(res, 201, { success: true, agent: newAgent });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Skill endpoints ---
|
|
125
|
+
|
|
126
|
+
if (pathname === '/api/skills' && method === 'GET') {
|
|
127
|
+
const userId = query.userId || 'default';
|
|
128
|
+
const skills = await skillManager.getUserAvailableSkills(userId);
|
|
129
|
+
sendJson(res, 200, skills);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (pathname === '/api/skills/search' && method === 'GET') {
|
|
134
|
+
const userId = query.userId || 'default';
|
|
135
|
+
const results = await skillManager.searchSkills(userId, query.q || '');
|
|
136
|
+
sendJson(res, 200, { results });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('[HTTP] Error:', error);
|
|
143
|
+
sendJson(res, 500, { error: error.message });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// --- WebSocket ---
|
|
148
|
+
|
|
149
|
+
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
|
|
150
|
+
|
|
151
|
+
wss.on('connection', (socket) => {
|
|
152
|
+
const runtime = new AgentRuntime({
|
|
153
|
+
workspacePath: config.workspacePath,
|
|
154
|
+
rules: config.rules,
|
|
155
|
+
mcpServers: config.mcpServers,
|
|
156
|
+
sessionsPath: config.sessionsPath,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const send = (payload) => {
|
|
160
|
+
try {
|
|
161
|
+
socket.send(JSON.stringify(payload));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[WS] Failed to send:', error.message);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
runtime.on('message', send);
|
|
168
|
+
send({ type: 'ready' });
|
|
169
|
+
|
|
170
|
+
socket.on('message', async (raw) => {
|
|
171
|
+
try {
|
|
172
|
+
const data = JSON.parse(raw.toString());
|
|
173
|
+
switch (data.type) {
|
|
174
|
+
case 'query':
|
|
175
|
+
await runtime.handleQuery(data.message, data.session_id || null, data.metadata || {});
|
|
176
|
+
break;
|
|
177
|
+
case 'new_session':
|
|
178
|
+
runtime.currentSessionId = null;
|
|
179
|
+
runtime.resetSessionState();
|
|
180
|
+
runtime.emitMessage({ type: 'info', message: 'Started new conversation' });
|
|
181
|
+
break;
|
|
182
|
+
case 'permission_response':
|
|
183
|
+
runtime.handlePermissionResponse(data);
|
|
184
|
+
break;
|
|
185
|
+
case 'rule_action':
|
|
186
|
+
await runtime.handleRuleActionMessage(data);
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
runtime.emitMessage({ type: 'error', message: `Unknown message type: ${data.type}` });
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
send({ type: 'error', message: error.message || 'Invalid payload' });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
socket.on('close', () => {
|
|
197
|
+
runtime.removeAllListeners('message');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
httpServer.listen(port, () => {
|
|
202
|
+
console.log(`Friday server listening on http://localhost:${port}`);
|
|
203
|
+
console.log(`WebSocket: ws://localhost:${port}/ws`);
|
|
204
|
+
console.log(`Workspace: ${config.workspacePath}`);
|
|
205
|
+
});
|
|
206
|
+
}
|