@hsafa/cli 0.0.1 → 0.0.3
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/README.md +3 -4
- package/dist/commands/agent.js +27 -12
- package/dist/commands/auth.js +49 -59
- package/dist/commands/workspace.js +0 -35
- package/dist/index.js +1 -3
- package/dist/utils/api.js +1 -1
- package/package.json +2 -2
- package/dist/commands/project.js +0 -90
package/README.md
CHANGED
|
@@ -29,17 +29,16 @@ hsafa auth status
|
|
|
29
29
|
hsafa auth logout
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
### Workspaces
|
|
32
|
+
### Workspaces
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
hsafa workspace list
|
|
36
|
-
hsafa workspace projects <workspace-id>
|
|
37
36
|
```
|
|
38
37
|
|
|
39
38
|
### Agents
|
|
40
39
|
|
|
41
40
|
```bash
|
|
42
|
-
hsafa agent list
|
|
41
|
+
hsafa agent list --workspace <workspace-id>
|
|
43
42
|
hsafa agent chat <agent-id>
|
|
44
43
|
```
|
|
45
44
|
|
|
@@ -67,6 +66,6 @@ hsafa status
|
|
|
67
66
|
## Features
|
|
68
67
|
|
|
69
68
|
- **Interactive Chat**: Real-time streaming conversation with agents.
|
|
70
|
-
- **
|
|
69
|
+
- **Workspace Navigation**: Easily list workspaces and agents.
|
|
71
70
|
- **Secure Auth**: Stores session tokens locally.
|
|
72
71
|
- **File Uploads**: Command-line file uploads to the server.
|
package/dist/commands/agent.js
CHANGED
|
@@ -157,29 +157,44 @@ export function registerAgentCommands(program) {
|
|
|
157
157
|
agent
|
|
158
158
|
.command('list')
|
|
159
159
|
.description('List all available agents')
|
|
160
|
-
.option('-
|
|
160
|
+
.option('-w, --workspace <id>', 'Workspace ID')
|
|
161
161
|
.action(async (options) => {
|
|
162
162
|
const globalOptions = program.opts();
|
|
163
163
|
const spinner = !globalOptions.json ? ora('Fetching agents...').start() : null;
|
|
164
164
|
try {
|
|
165
|
-
|
|
166
|
-
if (!
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
let workspaceId = options.workspace;
|
|
166
|
+
if (!workspaceId) {
|
|
167
|
+
const wsResponse = await api.get('/api/workspaces');
|
|
168
|
+
const workspaces = wsResponse.data;
|
|
169
|
+
if (!Array.isArray(workspaces) || workspaces.length === 0) {
|
|
170
|
+
if (spinner)
|
|
171
|
+
spinner.fail('No workspaces found.');
|
|
172
|
+
else
|
|
173
|
+
console.error(JSON.stringify({ error: 'No workspaces found' }));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
workspaceId = workspaces[0].id;
|
|
172
177
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
178
|
+
const query = `
|
|
179
|
+
query($workspaceId: ID!) {
|
|
180
|
+
agents(workspaceId: $workspaceId) {
|
|
181
|
+
id
|
|
182
|
+
name
|
|
183
|
+
description
|
|
184
|
+
status
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
const data = await graphqlRequest(query, { workspaceId });
|
|
189
|
+
const agents = data.agents;
|
|
175
190
|
if (spinner)
|
|
176
191
|
spinner.stop();
|
|
177
192
|
if (globalOptions.json) {
|
|
178
193
|
console.log(JSON.stringify(agents, null, 2));
|
|
179
194
|
return;
|
|
180
195
|
}
|
|
181
|
-
if (agents.length === 0) {
|
|
182
|
-
console.log(chalk.yellow('No agents found for this
|
|
196
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
197
|
+
console.log(chalk.yellow('No agents found for this workspace.'));
|
|
183
198
|
return;
|
|
184
199
|
}
|
|
185
200
|
const table = new Table({
|
package/dist/commands/auth.js
CHANGED
|
@@ -38,11 +38,38 @@ export function registerAuthCommands(program) {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
const spinner = ora('Waiting for authentication...').start();
|
|
41
|
-
//
|
|
41
|
+
// CSRF-ish state to prevent random localhost hits from completing login
|
|
42
|
+
const expectedState = Math.random().toString(36).slice(2);
|
|
43
|
+
// Start local server to receive the authorization code
|
|
42
44
|
const server = http.createServer(async (req, res) => {
|
|
43
45
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
+
const code = url.searchParams.get('code');
|
|
47
|
+
const returnedState = url.searchParams.get('state');
|
|
48
|
+
if (!returnedState || returnedState !== expectedState) {
|
|
49
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
50
|
+
res.end('Invalid state.');
|
|
51
|
+
spinner.fail(chalk.red('Login failed: Invalid state.'));
|
|
52
|
+
server.close();
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (!code) {
|
|
56
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
57
|
+
res.end('No code provided.');
|
|
58
|
+
spinner.fail(chalk.red('Login failed: No code received.'));
|
|
59
|
+
server.close();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Exchange code for token
|
|
63
|
+
try {
|
|
64
|
+
const response = await axios.post(`${serverUrl}/api/cli-auth/exchange`, { code }, { headers: { 'Content-Type': 'application/json' } });
|
|
65
|
+
const token = response?.data?.token;
|
|
66
|
+
if (!token || typeof token !== 'string') {
|
|
67
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
68
|
+
res.end('Invalid exchange response.');
|
|
69
|
+
spinner.fail(chalk.red('Login failed: Invalid token response.'));
|
|
70
|
+
server.close();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
46
73
|
// Save token
|
|
47
74
|
saveToken(token);
|
|
48
75
|
// Respond to browser
|
|
@@ -58,12 +85,13 @@ export function registerAuthCommands(program) {
|
|
|
58
85
|
`);
|
|
59
86
|
spinner.succeed(chalk.green('Login successful!'));
|
|
60
87
|
server.close();
|
|
61
|
-
process.exit(0);
|
|
88
|
+
process.exit(0);
|
|
62
89
|
}
|
|
63
|
-
|
|
90
|
+
catch (error) {
|
|
91
|
+
const errorMsg = error.response?.data?.error || error.message;
|
|
64
92
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
65
|
-
res.end(
|
|
66
|
-
spinner.fail(chalk.red(
|
|
93
|
+
res.end(`Login failed: ${errorMsg}`);
|
|
94
|
+
spinner.fail(chalk.red(`Login failed: ${errorMsg}`));
|
|
67
95
|
server.close();
|
|
68
96
|
process.exit(1);
|
|
69
97
|
}
|
|
@@ -73,66 +101,20 @@ export function registerAuthCommands(program) {
|
|
|
73
101
|
const address = server.address();
|
|
74
102
|
const port = address.port;
|
|
75
103
|
const callbackUrl = `http://localhost:${port}`;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// 1. Check if user is logged in (session).
|
|
80
|
-
// 2. If not, redirect to /api/auth/signin?callbackUrl=/cli-login?callback=callbackUrl
|
|
81
|
-
// 3. If logged in, get session token.
|
|
82
|
-
// 4. Redirect to callbackUrl?token=sessionToken
|
|
83
|
-
const loginUrl = `${serverUrl}/cli-login?callback=${encodeURIComponent(callbackUrl)}`;
|
|
104
|
+
const loginUrl = new URL('/cli-auth', serverUrl);
|
|
105
|
+
loginUrl.searchParams.set('callback', callbackUrl);
|
|
106
|
+
loginUrl.searchParams.set('state', expectedState);
|
|
84
107
|
try {
|
|
85
|
-
await open(loginUrl);
|
|
108
|
+
await open(loginUrl.toString());
|
|
86
109
|
}
|
|
87
110
|
catch (err) {
|
|
88
111
|
spinner.fail(chalk.red('Failed to open browser. Please open this URL manually:'));
|
|
89
|
-
console.log(loginUrl);
|
|
112
|
+
console.log(loginUrl.toString());
|
|
90
113
|
}
|
|
91
114
|
});
|
|
92
115
|
}
|
|
93
116
|
async function loginManual() {
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
type: 'input',
|
|
97
|
-
name: 'email',
|
|
98
|
-
message: 'Enter your email:',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
type: 'password',
|
|
102
|
-
name: 'password',
|
|
103
|
-
message: 'Enter your password:',
|
|
104
|
-
},
|
|
105
|
-
]);
|
|
106
|
-
const spinner = ora('Logging in...').start();
|
|
107
|
-
try {
|
|
108
|
-
const serverUrl = config.get('serverUrl');
|
|
109
|
-
const response = await axios.post(`${serverUrl}/api/auth/signin`, {
|
|
110
|
-
email: answers.email,
|
|
111
|
-
password: answers.password,
|
|
112
|
-
}, {
|
|
113
|
-
withCredentials: true,
|
|
114
|
-
});
|
|
115
|
-
// Extract cookie
|
|
116
|
-
const setCookie = response.headers['set-cookie'];
|
|
117
|
-
if (setCookie) {
|
|
118
|
-
const sessionTokenMatch = setCookie.find(c => c.includes('next-auth.session-token'));
|
|
119
|
-
if (sessionTokenMatch) {
|
|
120
|
-
const token = sessionTokenMatch.split(';')[0].split('=')[1];
|
|
121
|
-
saveToken(token);
|
|
122
|
-
spinner.succeed(chalk.green('Login successful!'));
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
spinner.fail(chalk.red('Login failed: Could not find session token in response.'));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
spinner.fail(chalk.red('Login failed: No cookies returned from server.'));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
const errorMsg = error.response?.data?.error || error.message;
|
|
134
|
-
spinner.fail(chalk.red(`Login failed: ${errorMsg}`));
|
|
135
|
-
}
|
|
117
|
+
console.log(chalk.yellow('Manual login is not supported. Please run `hsafa auth login` to authenticate via the browser.'));
|
|
136
118
|
}
|
|
137
119
|
function saveToken(token) {
|
|
138
120
|
config.set('token', token);
|
|
@@ -151,6 +133,14 @@ export function registerAuthCommands(program) {
|
|
|
151
133
|
.description('Logout from HSAFA')
|
|
152
134
|
.action(() => {
|
|
153
135
|
config.delete('token');
|
|
136
|
+
const currentProfile = config.get('currentProfile');
|
|
137
|
+
if (currentProfile) {
|
|
138
|
+
const profiles = config.get('profiles') || {};
|
|
139
|
+
if (profiles[currentProfile]) {
|
|
140
|
+
profiles[currentProfile].token = '';
|
|
141
|
+
config.set('profiles', profiles);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
154
144
|
console.log(chalk.green('Logged out successfully.'));
|
|
155
145
|
});
|
|
156
146
|
auth
|
|
@@ -39,39 +39,4 @@ export function registerWorkspaceCommands(program) {
|
|
|
39
39
|
console.error(JSON.stringify({ error: error.message }));
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
|
-
workspace
|
|
43
|
-
.command('projects <id>')
|
|
44
|
-
.description('List projects in a workspace')
|
|
45
|
-
.action(async (id) => {
|
|
46
|
-
const globalOptions = program.opts();
|
|
47
|
-
const spinner = !globalOptions.json ? ora('Fetching projects...').start() : null;
|
|
48
|
-
try {
|
|
49
|
-
const response = await api.get(`/api/workspaces/${id}/projects`);
|
|
50
|
-
const projects = response.data;
|
|
51
|
-
if (spinner)
|
|
52
|
-
spinner.stop();
|
|
53
|
-
if (globalOptions.json) {
|
|
54
|
-
console.log(JSON.stringify(projects, null, 2));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (projects.length === 0) {
|
|
58
|
-
console.log(chalk.yellow('No projects found in this workspace.'));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const table = new Table({
|
|
62
|
-
head: [chalk.cyan('ID'), chalk.cyan('Name'), chalk.cyan('Description')],
|
|
63
|
-
colWidths: [36, 20, 40]
|
|
64
|
-
});
|
|
65
|
-
projects.forEach((p) => {
|
|
66
|
-
table.push([p.id, p.name, p.description || 'N/A']);
|
|
67
|
-
});
|
|
68
|
-
console.log(table.toString());
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (spinner)
|
|
72
|
-
spinner.fail(`Failed to fetch projects: ${error.message}`);
|
|
73
|
-
else
|
|
74
|
-
console.error(JSON.stringify({ error: error.message }));
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
42
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import { registerDocCommands } from './commands/doc.js';
|
|
|
8
8
|
import { registerKbCommands } from './commands/kb.js';
|
|
9
9
|
import { registerKeyCommands } from './commands/key.js';
|
|
10
10
|
import { registerProfileCommands } from './commands/profile.js';
|
|
11
|
-
import { registerProjectCommands } from './commands/project.js';
|
|
12
11
|
import { registerSystemCommands } from './commands/system.js';
|
|
13
12
|
import { registerUserCommands } from './commands/user.js';
|
|
14
13
|
import { registerMemberCommands } from './commands/member.js';
|
|
@@ -21,7 +20,7 @@ const program = new Command();
|
|
|
21
20
|
program
|
|
22
21
|
.name('hsafa')
|
|
23
22
|
.description('CLI to control HSAFA without UI')
|
|
24
|
-
.version('0.0.
|
|
23
|
+
.version('0.0.3')
|
|
25
24
|
.option('--json', 'Output results in JSON format');
|
|
26
25
|
program
|
|
27
26
|
.command('config')
|
|
@@ -46,7 +45,6 @@ registerDocCommands(program);
|
|
|
46
45
|
registerKbCommands(program);
|
|
47
46
|
registerKeyCommands(program);
|
|
48
47
|
registerProfileCommands(program);
|
|
49
|
-
registerProjectCommands(program);
|
|
50
48
|
registerSystemCommands(program);
|
|
51
49
|
registerUserCommands(program);
|
|
52
50
|
registerMemberCommands(program);
|
package/dist/utils/api.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hsafa/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "CLI to control HSAFA without UI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc && cp -r src/templates dist/",
|
|
20
|
+
"build": "rm -rf dist && tsc && cp -r src/templates dist/ && node -e \"const fs=require('fs');const p='dist/index.js';let s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!/usr/bin/env node\\n')){fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s,{encoding:'utf8'});}\" && chmod +x dist/index.js",
|
|
21
21
|
"dev": "tsc --watch",
|
|
22
22
|
"start": "node dist/index.js",
|
|
23
23
|
"prepublishOnly": "pnpm run build"
|
package/dist/commands/project.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { graphqlRequest } from '../utils/graphql.js';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
export function registerProjectCommands(program) {
|
|
9
|
-
const project = program.command('project').description('Project management');
|
|
10
|
-
project
|
|
11
|
-
.command('init <name>')
|
|
12
|
-
.description('Initialize a new project with a template')
|
|
13
|
-
.option('-w, --workspace <id>', 'Workspace ID (if not provided, will use the first one)')
|
|
14
|
-
.option('-t, --template <name>', 'Template name (basic-assistant, researcher)', 'basic-assistant')
|
|
15
|
-
.action(async (name, options) => {
|
|
16
|
-
const spinner = ora('Initializing project...').start();
|
|
17
|
-
try {
|
|
18
|
-
// 1. Get Workspace ID
|
|
19
|
-
let workspaceId = options.workspace;
|
|
20
|
-
if (!workspaceId) {
|
|
21
|
-
const workspacesQuery = `query { workspaces { id name } }`;
|
|
22
|
-
const wsData = await graphqlRequest(workspacesQuery);
|
|
23
|
-
if (wsData.workspaces.length === 0) {
|
|
24
|
-
spinner.fail('No workspaces found. Create a workspace first using the UI or GraphQL.');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
workspaceId = wsData.workspaces[0].id;
|
|
28
|
-
spinner.info(`Using workspace: ${wsData.workspaces[0].name} (${workspaceId})`);
|
|
29
|
-
spinner.start('Creating project...');
|
|
30
|
-
}
|
|
31
|
-
// 2. Create Project
|
|
32
|
-
const createProjectMutation = `
|
|
33
|
-
mutation($input: CreateProjectInput!) {
|
|
34
|
-
createProject(input: $input) {
|
|
35
|
-
id
|
|
36
|
-
name
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
`;
|
|
40
|
-
// Note: The GraphQL schema for createProject might vary, let's assume it takes workspaceId
|
|
41
|
-
// Looking at current server code, projects are often created within a workspace scope.
|
|
42
|
-
// However, I'll use the createAgent mutation which we know works well.
|
|
43
|
-
// Let's create an agent directly in the workspace as per current hsafa server structure
|
|
44
|
-
const createAgentMutation = `
|
|
45
|
-
mutation($workspaceId: ID!, $input: CreateAgentInput!) {
|
|
46
|
-
createAgent(workspaceId: $workspaceId, input: $input) {
|
|
47
|
-
id
|
|
48
|
-
name
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
`;
|
|
52
|
-
const agentData = await graphqlRequest(createAgentMutation, {
|
|
53
|
-
workspaceId,
|
|
54
|
-
input: {
|
|
55
|
-
name,
|
|
56
|
-
description: `Project initialized from ${options.template} template`,
|
|
57
|
-
type: 'AI Agent'
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
const agentId = agentData.createAgent.id;
|
|
61
|
-
spinner.text = `Applying template ${options.template}...`;
|
|
62
|
-
// 3. Load Template
|
|
63
|
-
const templatePath = path.join(__dirname, '..', 'templates', `${options.template}.json`);
|
|
64
|
-
if (!fs.existsSync(templatePath)) {
|
|
65
|
-
spinner.fail(`Template ${options.template} not found.`);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
69
|
-
// 4. Apply Template Flow
|
|
70
|
-
const saveFlowMutation = `
|
|
71
|
-
mutation($agentId: ID!, $input: SaveAgentFlowInput!) {
|
|
72
|
-
saveAgentFlow(agentId: $agentId, input: $input)
|
|
73
|
-
}
|
|
74
|
-
`;
|
|
75
|
-
await graphqlRequest(saveFlowMutation, {
|
|
76
|
-
agentId,
|
|
77
|
-
input: {
|
|
78
|
-
nodes: template.nodes,
|
|
79
|
-
edges: template.edges
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
spinner.succeed(chalk.green(`Project "${name}" initialized successfully!`));
|
|
83
|
-
console.log(`\nAgent ID: ${chalk.cyan(agentId)}`);
|
|
84
|
-
console.log(`Run chat: ${chalk.bold(`hsafa agent chat ${agentId}`)}`);
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
spinner.fail(`Initialization failed: ${error.message}`);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|