@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 CHANGED
@@ -29,17 +29,16 @@ hsafa auth status
29
29
  hsafa auth logout
30
30
  ```
31
31
 
32
- ### Workspaces & Projects
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 -p <project-id>
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
- - **Project Navigation**: Easily list workspaces, projects, and agents.
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.
@@ -157,29 +157,44 @@ export function registerAgentCommands(program) {
157
157
  agent
158
158
  .command('list')
159
159
  .description('List all available agents')
160
- .option('-p, --project <id>', 'Project ID')
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
- const projectId = options.project;
166
- if (!projectId) {
167
- if (spinner)
168
- spinner.fail('Project ID is required. Use -p <id>');
169
- else
170
- console.error(JSON.stringify({ error: 'Project ID is required' }));
171
- return;
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 response = await api.get(`/api/projects/${projectId}/agents`);
174
- const agents = response.data;
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 project.'));
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({
@@ -38,11 +38,38 @@ export function registerAuthCommands(program) {
38
38
  return;
39
39
  }
40
40
  const spinner = ora('Waiting for authentication...').start();
41
- // Start local server to receive the token
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 token = url.searchParams.get('token');
45
- if (token) {
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); // Exit successfully
88
+ process.exit(0);
62
89
  }
63
- else {
90
+ catch (error) {
91
+ const errorMsg = error.response?.data?.error || error.message;
64
92
  res.writeHead(400, { 'Content-Type': 'text/plain' });
65
- res.end('No token provided.');
66
- spinner.fail(chalk.red('Login failed: No token received.'));
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
- // Open the browser
77
- // Point to the client-side CLI login page (where NextAuth is configured)
78
- // The client page logic:
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
- const answers = await inquirer.prompt([
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.1')
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
@@ -6,7 +6,7 @@ const api = axios.create({
6
6
  api.interceptors.request.use((req) => {
7
7
  const token = config.get('token');
8
8
  if (token) {
9
- req.headers.Cookie = `next-auth.session-token=${token}`;
9
+ req.headers.Authorization = `Bearer ${token}`;
10
10
  }
11
11
  return req;
12
12
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsafa/cli",
3
- "version": "0.0.1",
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"
@@ -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
- }