osai-agent 4.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/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/sound.js +33 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { ExitPromptError } from '@inquirer/core';
|
|
6
|
+
import { printAuthHeader, printError, clearScreen } from '../ui/terminal.js';
|
|
7
|
+
import { DEFAULT_SERVER_URL, toHttpUrl, toWsUrl } from '../services/server-url.js';
|
|
8
|
+
import { run } from './run.js';
|
|
9
|
+
import pkg from 'node-machine-id';
|
|
10
|
+
const { machineIdSync } = pkg;
|
|
11
|
+
|
|
12
|
+
const attemptRegister = async (email, password, serverInput, config) => {
|
|
13
|
+
const server = toWsUrl(serverInput);
|
|
14
|
+
const machineId = machineIdSync();
|
|
15
|
+
const spinner = ora('Creating account...').start();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(`${toHttpUrl(server)}/auth/register`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ email, password, machineId })
|
|
22
|
+
});
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
if (!response.ok) { spinner.fail('Creation refused'); printError(data.error || 'Error during registration'); return 'auth_error'; }
|
|
25
|
+
config.set({ token: data.token, userId: data.user.id, plan: data.user.plan, server });
|
|
26
|
+
spinner.succeed('Account created');
|
|
27
|
+
return 'success';
|
|
28
|
+
} catch {
|
|
29
|
+
spinner.fail('Unable to create account');
|
|
30
|
+
return 'network_error';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const register = async ({ server: serverOverride } = {}) => {
|
|
35
|
+
const config = new Conf({ projectName: 'osai-agent' });
|
|
36
|
+
const token = config.get('token');
|
|
37
|
+
if (token) {
|
|
38
|
+
console.log(chalk.yellow('Already logged in. Use osai-agent logout first.'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const prefixTheme = { idle: chalk.hex('#7aa2f7')('▸'), done: chalk.hex('#7aa2f7')('▸') };
|
|
42
|
+
clearScreen();
|
|
43
|
+
console.log();
|
|
44
|
+
printAuthHeader(chalk.white('Create Your Account'), 'Join OS AI Agent and get started with AI-powered automation.');
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
let serverInput = serverOverride || config.get('server') || DEFAULT_SERVER_URL;
|
|
48
|
+
|
|
49
|
+
let email, password;
|
|
50
|
+
try {
|
|
51
|
+
({ email, password } = await inquirer.prompt([
|
|
52
|
+
{ type: 'input', name: 'email', message: 'Email \u203A', theme: { prefix: prefixTheme }, validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'Invalid email' },
|
|
53
|
+
{ type: 'password', name: 'password', message: 'Password \u203A', theme: { prefix: prefixTheme }, mask: '*', validate: (value) => value.length >= 8 || 'Minimum 8 characters' },
|
|
54
|
+
]));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof ExitPromptError) process.exit(0);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = await attemptRegister(email, password, serverInput, config);
|
|
61
|
+
if (result === 'success') { await run({}); return; }
|
|
62
|
+
if (result !== 'network_error') return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const { serverInput: retryServer } = await inquirer.prompt([
|
|
66
|
+
{ type: 'input', name: 'serverInput', message: 'Server \u203A', theme: { prefix: prefixTheme }, default: serverInput }
|
|
67
|
+
]);
|
|
68
|
+
const result2 = await attemptRegister(email, password, retryServer, config);
|
|
69
|
+
if (result2 === 'success') await run({});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err instanceof ExitPromptError) process.exit(0);
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import { App } from '../ui/App.js';
|
|
6
|
+
import { h } from '../ui/h.js';
|
|
7
|
+
import { AgentLoop } from '../agent/react-loop.js';
|
|
8
|
+
import { SessionManager } from '../services/session.js';
|
|
9
|
+
import { detectDefaultOS } from './config.js';
|
|
10
|
+
import { toHttpUrl } from '../services/server-url.js';
|
|
11
|
+
import { MODES } from '../utils/constants.js';
|
|
12
|
+
import { printNotLoggedIn } from '../ui/terminal.js';
|
|
13
|
+
import { disableMouseReporting } from '../ui/mouse-scroll.js';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
|
|
16
|
+
export const run = async ({ server: serverOverride, noConfirm = false, osOverride, coding = false, fromSession = null, local = false } = {}) => {
|
|
17
|
+
const config = new Conf({ projectName: 'osai-agent' });
|
|
18
|
+
const token = config.get('token');
|
|
19
|
+
const server = toHttpUrl(serverOverride ? `ws://${serverOverride}` : config.get('server'));
|
|
20
|
+
|
|
21
|
+
const effectiveLocal = local || (fromSession?.local ?? false);
|
|
22
|
+
|
|
23
|
+
if (!effectiveLocal) {
|
|
24
|
+
if (!token) {
|
|
25
|
+
printNotLoggedIn();
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (!server) {
|
|
29
|
+
console.error('Server URL not configured. Run: osai-agent login');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resumedMode = Object.values(MODES).includes(fromSession?.mode) ? fromSession.mode : null;
|
|
35
|
+
const resumedOS = fromSession?.os;
|
|
36
|
+
const userOS = osOverride || resumedOS || config.get('os') || detectDefaultOS();
|
|
37
|
+
const startMode = coding ? MODES.CODING : (resumedMode || config.get('mode') || MODES.GENERAL);
|
|
38
|
+
|
|
39
|
+
const agentConfig = {
|
|
40
|
+
mode: startMode,
|
|
41
|
+
executionMode: config.get('executionMode') === 'PLAN' ? 'PLAN' : 'EXEC',
|
|
42
|
+
local: effectiveLocal,
|
|
43
|
+
server,
|
|
44
|
+
token,
|
|
45
|
+
noConfirm,
|
|
46
|
+
device: userOS,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let agentLoopRef = null;
|
|
50
|
+
|
|
51
|
+
const createAgentLoop = (options) => {
|
|
52
|
+
agentLoopRef = new AgentLoop(options);
|
|
53
|
+
return agentLoopRef;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Désactiver XON/XOFF immédiatement (empêche Ctrl+S de geler le terminal)
|
|
57
|
+
// et désactiver le caractère discard (Ctrl+O) que le terminal intercepte sinon
|
|
58
|
+
if (process.stdout.isTTY) {
|
|
59
|
+
try { execSync('stty sane', { stdio: 'ignore' }); } catch {}
|
|
60
|
+
try { execSync('stty -ixon', { stdio: 'ignore' }); } catch {}
|
|
61
|
+
try { execSync('stty discard undef', { stdio: 'ignore' }); } catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Restauration du terminal en cas de signal inattendu
|
|
65
|
+
const restoreTerminal = () => {
|
|
66
|
+
try {
|
|
67
|
+
disableMouseReporting();
|
|
68
|
+
process.stdout.write('\x1B[?25h');
|
|
69
|
+
process.stdout.write('\x1B[?7h');
|
|
70
|
+
if (process.stdin.isTTY) process.stdin.setRawMode?.(false);
|
|
71
|
+
if (process.stdout.isTTY) {
|
|
72
|
+
try { execSync('stty sane', { stdio: 'ignore' }); } catch {}
|
|
73
|
+
try { execSync('stty -ixon', { stdio: 'ignore' }); } catch {}
|
|
74
|
+
try { execSync('stty discard undef', { stdio: 'ignore' }); } catch {}
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const sigHandler = () => {
|
|
80
|
+
let savedId = null;
|
|
81
|
+
if (agentLoopRef) {
|
|
82
|
+
try {
|
|
83
|
+
const history = agentLoopRef.getConversationHistory();
|
|
84
|
+
if (history?.length > 0) {
|
|
85
|
+
const sm = new SessionManager({ server: agentConfig.server, token: agentConfig.token });
|
|
86
|
+
savedId = sm.generateSessionId();
|
|
87
|
+
sm.saveLocalSync(savedId, {
|
|
88
|
+
title: `Session ${savedId}`,
|
|
89
|
+
conversationHistory: history,
|
|
90
|
+
mode: agentConfig.mode,
|
|
91
|
+
os: agentConfig.device,
|
|
92
|
+
local: agentConfig.local,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
restoreTerminal();
|
|
98
|
+
if (savedId) {
|
|
99
|
+
process.stdout.write(`\n${chalk.dim('Session saved. Resume with:')} osai-agent run ${savedId}\n\n`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(0);
|
|
102
|
+
};
|
|
103
|
+
process.prependListener('SIGINT', sigHandler);
|
|
104
|
+
process.prependListener('SIGTERM', sigHandler);
|
|
105
|
+
|
|
106
|
+
process.stdout.write('\x1Bc');
|
|
107
|
+
await new Promise(r => setTimeout(r, 100));
|
|
108
|
+
// [FIX-4] Throttle adaptatif : sur Linux, on abaisse la cadence de rendu à 10 fps
|
|
109
|
+
// pour éviter le scintillement (redraw complet trop fréquent). OSAI_MAX_FPS permet
|
|
110
|
+
// d'override depuis l'environnement (ex: OSAI_MAX_FPS=8 sur un terminal très lent).
|
|
111
|
+
const isLinux = process.platform === 'linux';
|
|
112
|
+
const envFps = Number.parseInt(process.env.OSAI_MAX_FPS || '', 10);
|
|
113
|
+
const maxFps = Number.isFinite(envFps) && envFps > 0 ? envFps : (isLinux ? 10 : 15);
|
|
114
|
+
logger.debug('Render config', { maxFps, platform: process.platform, isTTY: !!process.stdout.isTTY });
|
|
115
|
+
|
|
116
|
+
let clear, waitUntilExit;
|
|
117
|
+
try {
|
|
118
|
+
({ clear, waitUntilExit } = render(h(App, {
|
|
119
|
+
createAgentLoop,
|
|
120
|
+
agentConfig,
|
|
121
|
+
initialSession: fromSession || null,
|
|
122
|
+
onExit: () => {
|
|
123
|
+
clear();
|
|
124
|
+
restoreTerminal();
|
|
125
|
+
process.removeListener('SIGINT', sigHandler);
|
|
126
|
+
process.removeListener('SIGTERM', sigHandler);
|
|
127
|
+
if (agentLoopRef) {
|
|
128
|
+
try { agentLoopRef.cleanup(); } catch {}
|
|
129
|
+
}
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
}), {
|
|
133
|
+
incrementalRendering: true,
|
|
134
|
+
maxFps,
|
|
135
|
+
}));
|
|
136
|
+
} catch {
|
|
137
|
+
restoreTerminal();
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ink appelle setRawMode(true) en interne, ce qui peut réactiver IXON.
|
|
142
|
+
// On re-désactive XON/XOFF après l'initialisation pour garantir la fluidité de l'input.
|
|
143
|
+
// On re-désactive aussi discard (Ctrl+O) que le terminal intercepte sinon.
|
|
144
|
+
if (process.stdout.isTTY) {
|
|
145
|
+
try { execSync('stty -ixon', { stdio: 'ignore' }); } catch {}
|
|
146
|
+
try { execSync('stty discard undef', { stdio: 'ignore' }); } catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await waitUntilExit;
|
|
150
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import { printError, printSuccess, printInfo } from '../ui/terminal.js';
|
|
4
|
+
|
|
5
|
+
const getConfig = () => new Conf({ projectName: 'osai-agent' });
|
|
6
|
+
|
|
7
|
+
const SEARCH_PROVIDERS = ['serpapi', 'tavily'];
|
|
8
|
+
|
|
9
|
+
const maskKey = (key) => {
|
|
10
|
+
if (!key) return null;
|
|
11
|
+
if (key.length <= 4) return key;
|
|
12
|
+
return key.slice(0, 3) + '...' + key.slice(-4);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const searchSet = async (provider, options) => {
|
|
16
|
+
if (!provider) {
|
|
17
|
+
printError('Usage: osai-agent search set <provider> --key <key>');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalized = provider.toLowerCase();
|
|
22
|
+
if (!SEARCH_PROVIDERS.includes(normalized)) {
|
|
23
|
+
printError(`Unknown search provider: ${provider}. Supported: ${SEARCH_PROVIDERS.join(', ')}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const key = options.key || options.k;
|
|
28
|
+
if (!key) {
|
|
29
|
+
printError(`Usage: osai-agent search set ${provider} --key <key>`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
const searchKeys = config.get('searchApiKeys', {});
|
|
35
|
+
searchKeys[normalized] = key;
|
|
36
|
+
config.set('searchApiKeys', searchKeys);
|
|
37
|
+
|
|
38
|
+
printSuccess(`Search API key saved for ${normalized} | Key: ${maskKey(key)}`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const searchList = async () => {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const searchKeys = config.get('searchApiKeys', {});
|
|
44
|
+
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(chalk.hex('#7aa2f7').bold(' Search Providers'));
|
|
47
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(32)));
|
|
48
|
+
|
|
49
|
+
if (Object.keys(searchKeys).length === 0) {
|
|
50
|
+
console.log(chalk.hex('#565f89')(' No search API keys configured.'));
|
|
51
|
+
console.log(chalk.hex('#565f89')(` Use: osai-agent search set <provider> --key <key>`));
|
|
52
|
+
console.log();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [provider, key] of Object.entries(searchKeys)) {
|
|
57
|
+
const ddgInfo = provider === 'serpapi' || provider === 'tavily' ? '' : '';
|
|
58
|
+
console.log(` ${chalk.white(provider.padEnd(20))} ${chalk.gray(maskKey(key))}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(32)));
|
|
62
|
+
console.log(chalk.gray(' DuckDuckGo is always available (no key needed)'));
|
|
63
|
+
console.log();
|
|
64
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { SessionManager } from '../services/session.js';
|
|
3
|
+
import { printInfo, printError, printSuccess } from '../ui/terminal.js';
|
|
4
|
+
import { run } from './run.js';
|
|
5
|
+
|
|
6
|
+
const sessionManager = new SessionManager();
|
|
7
|
+
|
|
8
|
+
export const manageSessions = async (subcommand, rest) => {
|
|
9
|
+
switch (subcommand) {
|
|
10
|
+
case 'list': {
|
|
11
|
+
const sessions = await sessionManager.listAll();
|
|
12
|
+
if (!sessions.length) { printInfo('No saved sessions'); return; }
|
|
13
|
+
console.log();
|
|
14
|
+
for (const s of sessions) {
|
|
15
|
+
const date = new Date(s.savedAt).toLocaleString();
|
|
16
|
+
console.log(chalk.hex('#7aa2f7')(` ${s.id}`) + chalk.hex('#565f89')(` ${s.mode} · ${s.os} · ${s.messageCount} msgs · ${date}`));
|
|
17
|
+
}
|
|
18
|
+
console.log();
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'export': {
|
|
22
|
+
const sessionId = rest[0];
|
|
23
|
+
if (!sessionId) { printError('Usage: osai-agent session export <id>'); return; }
|
|
24
|
+
const content = await sessionManager.exportSession(sessionId, rest[1] || 'markdown');
|
|
25
|
+
if (!content) { printError('Session not found'); return; }
|
|
26
|
+
console.log(content);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'delete': {
|
|
30
|
+
const sessionId = rest[0];
|
|
31
|
+
if (!sessionId) { printError('Usage: osai-agent session delete <id>'); return; }
|
|
32
|
+
const deleted = await sessionManager.deleteSession(sessionId);
|
|
33
|
+
if (deleted) printSuccess('Session deleted');
|
|
34
|
+
else printError('Session not found');
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'load': {
|
|
38
|
+
const sessionId = rest[0];
|
|
39
|
+
if (!sessionId) { printError('Usage: osai-agent session load <id>'); return; }
|
|
40
|
+
const sessionData = await sessionManager.load(sessionId);
|
|
41
|
+
if (!sessionData) { printError(`Session not found: ${sessionId}`); return; }
|
|
42
|
+
printSuccess(`Resuming session: ${sessionId} (${sessionData.mode || 'GENERAL'}, ${sessionData.messageCount || (sessionData.conversationHistory?.length || 0)} messages)`);
|
|
43
|
+
const serverOverride = rest.find(a => a.startsWith('--server='))?.split('=')[1];
|
|
44
|
+
await run({ server: serverOverride, fromSession: { ...sessionData, id: sessionId } });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
default: {
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.hex('#7aa2f7').bold('Session Management'));
|
|
50
|
+
console.log(chalk.hex('#565f89')(' osai-agent session list - List saved sessions'));
|
|
51
|
+
console.log(chalk.hex('#565f89')(' osai-agent session export <id> - Export session (markdown/json)'));
|
|
52
|
+
console.log(chalk.hex('#565f89')(' osai-agent session delete <id> - Delete a session'));
|
|
53
|
+
console.log(chalk.hex('#565f89')(' osai-agent session load <id> - Resume a session'));
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { discoverSkills, loadSkill, formatSkillsList, invalidateSkillsCache } from '../skills/loader.js';
|
|
3
|
+
import { printError } from '../ui/terminal.js';
|
|
4
|
+
|
|
5
|
+
export async function manageSkills(subcommand, args = []) {
|
|
6
|
+
invalidateSkillsCache();
|
|
7
|
+
const skills = await discoverSkills({ refresh: true });
|
|
8
|
+
|
|
9
|
+
switch (subcommand) {
|
|
10
|
+
case 'list': {
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(chalk.hex('#7aa2f7').bold('Available Skills'));
|
|
13
|
+
console.log();
|
|
14
|
+
if (!skills.length) {
|
|
15
|
+
console.log(chalk.gray(' No skills found.'));
|
|
16
|
+
console.log(chalk.gray(' Create: ~/.osai-agent/skills/<name>/SKILL.md'));
|
|
17
|
+
console.log(chalk.gray(' or: .osai-agent/skills/<name>/SKILL.md'));
|
|
18
|
+
} else {
|
|
19
|
+
for (const s of skills) {
|
|
20
|
+
console.log(` ${chalk.white(s.name.padEnd(24))}${chalk.gray(s.description)}`);
|
|
21
|
+
console.log(chalk.hex('#565f89')(` [${s.source}] ${s.path}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
console.log();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case 'show': {
|
|
29
|
+
const name = args[0];
|
|
30
|
+
if (!name) {
|
|
31
|
+
printError('Usage: osai-agent skills show <name>');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const result = await loadSkill(name);
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
printError(result.error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
console.log(result.output);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(chalk.hex('#7aa2f7').bold('Skills Management'));
|
|
46
|
+
console.log(chalk.hex('#565f89')(' osai-agent skills list - List discovered skills'));
|
|
47
|
+
console.log(chalk.hex('#565f89')(' osai-agent skills show <name> - Show skill content'));
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.gray(' Skill directories:'));
|
|
50
|
+
console.log(chalk.gray(' ~/.osai-agent/skills/<name>/SKILL.md'));
|
|
51
|
+
console.log(chalk.gray(' .osai-agent/skills/<name>/SKILL.md'));
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import boxen from 'boxen';
|
|
4
|
+
import { showConfig } from './config.js';
|
|
5
|
+
import { clearScreen, printError, printPanel } from '../ui/terminal.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { ENV_VARS, APP_VERSION } from '../utils/constants.js';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
|
|
11
|
+
export const stopSubagent = async ({ server }) => {
|
|
12
|
+
clearScreen();
|
|
13
|
+
// Show header similar to other commands
|
|
14
|
+
const config = new Conf({ projectName: 'osai-agent' });
|
|
15
|
+
const token = config.get('token');
|
|
16
|
+
const serverUrl = server || config.get('server') || 'https://agent.osai.dev';
|
|
17
|
+
// Default from env
|
|
18
|
+
const authDot = token ? chalk.green('●') : chalk.red('○');
|
|
19
|
+
const authLabel = token ? 'Authenticated' : 'Not authenticated';
|
|
20
|
+
const serverHost = serverUrl.replace(/^ws(s?):\/\//, '');
|
|
21
|
+
const headerContent = [
|
|
22
|
+
` ${chalk.hex('#4a9eff').bold('OS AI AGENT')} ${chalk.gray(`v${APP_VERSION}`)}`,
|
|
23
|
+
'',
|
|
24
|
+
` ${chalk.hex('#8ab4d8')('Your AI sysadmin, network engineer & senior developer')}`,
|
|
25
|
+
'',
|
|
26
|
+
` ${authDot} ${chalk.hex('#8ab4d8')('Auth:')} ${chalk.white(authLabel)} ${chalk.hex('#8ab4d8')('Server:')} ${chalk.white(serverHost)}`,
|
|
27
|
+
].join('\n');
|
|
28
|
+
console.log(boxen(headerContent, { padding: { left: 3, right: 3 }, margin: { top: 1, bottom: 1 }, borderStyle: 'round', borderColor: '#4a9eff', float: 'center', }));
|
|
29
|
+
|
|
30
|
+
if (!token) {
|
|
31
|
+
printError('You need to be logged in to stop a subagent. Please run "osai-agent login" first.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const spinner = ora('Stopping subagent...').start();
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`${serverUrl}/agent/stop-subagent`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'Authorization': `Bearer ${token}`
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const error = await response.text();
|
|
46
|
+
spinner.fail(`Failed to stop subagent: ${response.status} - ${error}`);
|
|
47
|
+
console.error(chalk.red(`Error: ${response.status} - ${error}`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const result = await response.json();
|
|
51
|
+
spinner.succeeded(result.message || 'Subagent stop requested');
|
|
52
|
+
console.log(chalk.green('✓ Subagent stop command sent successfully'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
spinner.fail(`Failed to stop subagent: ${error.message}`);
|
|
55
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { run } from './commands/run.js';
|
|
4
|
+
import { login } from './commands/login.js';
|
|
5
|
+
import { register } from './commands/register.js';
|
|
6
|
+
import { logout } from './commands/logout.js';
|
|
7
|
+
import { connect } from './commands/connect.js';
|
|
8
|
+
import { listDevices, addDevice, removeDevice } from './commands/devices.js';
|
|
9
|
+
import { showConfig, showVersion, setOS, showOS } from './commands/config.js';
|
|
10
|
+
import {
|
|
11
|
+
setProvider,
|
|
12
|
+
showProviderModels,
|
|
13
|
+
showProvider,
|
|
14
|
+
resetProvider,
|
|
15
|
+
addCustomProvider,
|
|
16
|
+
listProviders,
|
|
17
|
+
listConfiguredProviders,
|
|
18
|
+
changeModel,
|
|
19
|
+
changeKey,
|
|
20
|
+
switchProvider,
|
|
21
|
+
} from './commands/provider.js';
|
|
22
|
+
import { manageSessions } from './commands/session.js';
|
|
23
|
+
import { searchSet, searchList } from './commands/search.js';
|
|
24
|
+
import { manageSkills } from './commands/skills.js';
|
|
25
|
+
import { handleMcpCommand } from './commands/mcp.js';
|
|
26
|
+
import { stopSubagent } from './commands/stop-subagent.js';
|
|
27
|
+
import minimist from 'minimist';
|
|
28
|
+
|
|
29
|
+
const args = minimist(process.argv.slice(2));
|
|
30
|
+
const cmd = args._[0];
|
|
31
|
+
|
|
32
|
+
switch (cmd) {
|
|
33
|
+
case 'run':
|
|
34
|
+
await run({
|
|
35
|
+
server: args.server,
|
|
36
|
+
noConfirm: args['no-confirm'],
|
|
37
|
+
osOverride: args.os,
|
|
38
|
+
coding: args.coding,
|
|
39
|
+
fromSession: args['from-session'],
|
|
40
|
+
local: args.local,
|
|
41
|
+
});
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'login':
|
|
45
|
+
await login({ server: args.server });
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'register':
|
|
49
|
+
await register({ server: args.server });
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'logout':
|
|
53
|
+
logout();
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'connect':
|
|
57
|
+
await connect({
|
|
58
|
+
device: args.device,
|
|
59
|
+
server: args.server,
|
|
60
|
+
noConfirm: args['no-confirm'],
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'devices':
|
|
65
|
+
if (args._[1] === 'add') {
|
|
66
|
+
await addDevice();
|
|
67
|
+
} else if (args._[1] === 'remove') {
|
|
68
|
+
await removeDevice(args._[2]);
|
|
69
|
+
} else {
|
|
70
|
+
await listDevices();
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'config':
|
|
75
|
+
if (args._[1] === 'os') {
|
|
76
|
+
if (args._[2]) {
|
|
77
|
+
await setOS(args._[2]);
|
|
78
|
+
} else {
|
|
79
|
+
showOS();
|
|
80
|
+
}
|
|
81
|
+
} else if (args._[1]) {
|
|
82
|
+
showConfig(args._[1]);
|
|
83
|
+
} else {
|
|
84
|
+
showConfig();
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'version':
|
|
89
|
+
case '--version':
|
|
90
|
+
showVersion();
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'provider':
|
|
94
|
+
await handleProviderCommand(args);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'session':
|
|
98
|
+
case 'sessions':
|
|
99
|
+
await manageSessions(args._[1], args._.slice(2));
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'search':
|
|
103
|
+
if (args._[1] === 'set') {
|
|
104
|
+
await searchSet(args._[2], args);
|
|
105
|
+
} else {
|
|
106
|
+
await searchList();
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case 'skills':
|
|
111
|
+
await manageSkills(args._[1], args._.slice(2));
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case 'mcp':
|
|
115
|
+
await handleMcpCommand(args);
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'stop-subagent':
|
|
119
|
+
await stopSubagent({ server: args.server });
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
if (cmd) {
|
|
124
|
+
console.error(`Unknown command: ${cmd}`);
|
|
125
|
+
}
|
|
126
|
+
console.log();
|
|
127
|
+
console.log('Usage: osai-agent <command> [options]');
|
|
128
|
+
console.log();
|
|
129
|
+
console.log('Commands:');
|
|
130
|
+
console.log(' run Start the interactive agent');
|
|
131
|
+
console.log(' login Authenticate with the server');
|
|
132
|
+
console.log(' register Create a new account');
|
|
133
|
+
console.log(' logout Clear authentication');
|
|
134
|
+
console.log(' connect Connect to a remote device');
|
|
135
|
+
console.log(' devices Manage remote devices');
|
|
136
|
+
console.log(' config View or change configuration');
|
|
137
|
+
console.log(' version Show version');
|
|
138
|
+
console.log(' provider Manage provider settings');
|
|
139
|
+
console.log(' session Manage conversation sessions');
|
|
140
|
+
console.log(' search Configure search provider');
|
|
141
|
+
console.log(' skills Manage skills');
|
|
142
|
+
console.log(' mcp Manage MCP servers');
|
|
143
|
+
console.log(' stop-subagent Stop a running subagent');
|
|
144
|
+
console.log();
|
|
145
|
+
process.exit(cmd ? 1 : 0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleProviderCommand(args) {
|
|
149
|
+
const sub = args._[1];
|
|
150
|
+
|
|
151
|
+
switch (sub) {
|
|
152
|
+
case 'set':
|
|
153
|
+
await setProvider(args._[2], {
|
|
154
|
+
key: args.key,
|
|
155
|
+
model: args.model,
|
|
156
|
+
url: args.url,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
case 'models':
|
|
160
|
+
await showProviderModels(args._[2]);
|
|
161
|
+
break;
|
|
162
|
+
case 'show':
|
|
163
|
+
await showProvider();
|
|
164
|
+
break;
|
|
165
|
+
case 'reset':
|
|
166
|
+
await resetProvider();
|
|
167
|
+
break;
|
|
168
|
+
case 'custom':
|
|
169
|
+
await addCustomProvider({
|
|
170
|
+
name: args.name,
|
|
171
|
+
key: args.key,
|
|
172
|
+
url: args.url,
|
|
173
|
+
model: args.model,
|
|
174
|
+
});
|
|
175
|
+
break;
|
|
176
|
+
case 'list':
|
|
177
|
+
await listProviders();
|
|
178
|
+
break;
|
|
179
|
+
case 'configured':
|
|
180
|
+
await listConfiguredProviders();
|
|
181
|
+
break;
|
|
182
|
+
case 'model':
|
|
183
|
+
await changeModel(args._[2], args._[3]);
|
|
184
|
+
break;
|
|
185
|
+
case 'key':
|
|
186
|
+
await changeKey(args._[2], args._[3]);
|
|
187
|
+
break;
|
|
188
|
+
case 'switch':
|
|
189
|
+
await switchProvider(args._[2]);
|
|
190
|
+
break;
|
|
191
|
+
default:
|
|
192
|
+
console.log();
|
|
193
|
+
console.log('Usage: osai-agent provider <subcommand> [options]');
|
|
194
|
+
console.log();
|
|
195
|
+
console.log('Subcommands:');
|
|
196
|
+
console.log(' set <type> Set provider (--key, --model, --url)');
|
|
197
|
+
console.log(' models <type> List available models for a provider');
|
|
198
|
+
console.log(' show Show current provider');
|
|
199
|
+
console.log(' reset Reset to default provider');
|
|
200
|
+
console.log(' custom Add custom provider (--name, --key, --url, --model)');
|
|
201
|
+
console.log(' list List available providers');
|
|
202
|
+
console.log(' configured List configured providers');
|
|
203
|
+
console.log(' model <type> <m> Change model for a configured provider');
|
|
204
|
+
console.log(' key <type> <k> Change API key for a configured provider');
|
|
205
|
+
console.log(' switch <type> Switch to a configured provider');
|
|
206
|
+
console.log();
|
|
207
|
+
}
|
|
208
|
+
}
|