openkbs 0.0.51 → 0.0.53

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.
@@ -2,6 +2,7 @@ const https = require('https');
2
2
  const fs = require('fs');
3
3
  const readline = require('readline');
4
4
  const path = require('path');
5
+ const { URL } = require('url');
5
6
 
6
7
  class OpenKBSAgentClient {
7
8
  constructor() {
@@ -12,7 +13,7 @@ class OpenKBSAgentClient {
12
13
 
13
14
  findSettings() {
14
15
  let currentDir = __dirname;
15
-
16
+
16
17
  while (currentDir !== path.parse(currentDir).root) {
17
18
  const settingsPath = path.join(currentDir, 'app', 'settings.json');
18
19
  if (fs.existsSync(settingsPath)) {
@@ -20,13 +21,13 @@ class OpenKBSAgentClient {
20
21
  }
21
22
  currentDir = path.dirname(currentDir);
22
23
  }
23
-
24
+
24
25
  throw new Error('Could not find app/settings.json in parent directories');
25
26
  }
26
27
 
27
28
  findSecretsPath() {
28
29
  let currentDir = __dirname;
29
-
30
+
30
31
  while (currentDir !== path.parse(currentDir).root) {
31
32
  const settingsPath = path.join(currentDir, 'app', 'settings.json');
32
33
  if (fs.existsSync(settingsPath)) {
@@ -35,24 +36,28 @@ class OpenKBSAgentClient {
35
36
  }
36
37
  currentDir = path.dirname(currentDir);
37
38
  }
38
-
39
+
39
40
  throw new Error('Could not find agent directory with app/settings.json');
40
41
  }
41
42
 
42
43
  async getApiKey() {
43
44
  if (this.apiKey) return this.apiKey;
44
-
45
+
45
46
  if (fs.existsSync(this.secretsPath)) {
46
47
  const secrets = JSON.parse(fs.readFileSync(this.secretsPath, 'utf8'));
47
48
  this.apiKey = secrets.apiKey;
48
49
  return this.apiKey;
49
50
  }
50
-
51
+
51
52
  this.apiKey = await this.promptForApiKey();
52
53
  this.saveApiKey(this.apiKey);
53
54
  return this.apiKey;
54
55
  }
55
56
 
57
+ async init() {
58
+ await this.getApiKey();
59
+ }
60
+
56
61
  async promptForApiKey() {
57
62
  return new Promise((resolve) => {
58
63
  const rl = readline.createInterface({
@@ -89,89 +94,170 @@ class OpenKBSAgentClient {
89
94
  fs.writeFileSync(this.secretsPath, JSON.stringify({ apiKey: key }, null, 2));
90
95
  }
91
96
 
97
+ /**
98
+ * Run a job and get response from the agent
99
+ *
100
+ * @param {string|Array} message - Either:
101
+ * - String: "Do something ..."
102
+ * - Array: [
103
+ * {type:"text", text:"Process this invoice"},
104
+ * {type:"image_url", image_url:{url:"https://files.openkbs.com/invoice.png"}}
105
+ * ]
106
+ *
107
+ * @returns {Promise<Object>} Response structure:
108
+ * {
109
+ * data: {
110
+ * type: 'TEXT' | 'CUSTOM_TYPE',
111
+ * content: '...' | data: {...}
112
+ * },
113
+ * chatId: 'xxx-xxx',
114
+ * msgId: 'msg_xxx'
115
+ * }
116
+ */
92
117
  async runJob(message, options = {}) {
93
118
  const apiKey = await this.getApiKey();
94
-
95
- if (!this.settings.kbId) {
96
- throw new Error('First use: "openkbs push" to create the agent');
97
- }
119
+ if (!this.settings.kbId) throw new Error('First use: "openkbs push" to create the agent');
98
120
 
99
- const chatTitle = options.chatTitle || `Task ${new Date().getTime()}`;
100
- const chatId = await this.startJob(chatTitle, message, { kbId: this.settings.kbId, apiKey });
101
-
102
- console.log(`Job ${chatId} created.\nWorking ...`);
103
-
104
- if (options.poll !== false) {
105
- return this.pollForMessages(chatId, { kbId: this.settings.kbId, apiKey });
106
- }
107
-
108
- return chatId;
121
+ const payload = { message };
122
+ Object.keys(options).forEach(key => {
123
+ if (key === 'historyLimit') {
124
+ payload[key] = Math.min(Math.max(1, options[key]), 100);
125
+ } else if (options[key] !== undefined) {
126
+ payload[key] = options[key];
127
+ }
128
+ });
129
+
130
+ const response = await this.request(
131
+ `https://${this.settings.kbId}.apps.openkbs.com/api`,
132
+ payload,
133
+ { Authorization: `Bearer ${apiKey}` }
134
+ );
135
+
136
+ if (response.chatId) this.lastChatId = response.chatId;
137
+ return response;
109
138
  }
110
139
 
111
- async startJob(chatTitle, data, app) {
112
- const response = await this.makeRequest('https://chat.openkbs.com/', {
113
- ...app,
114
- chatTitle,
115
- message: data
140
+ async continueChat(message, chatId = null, options = {}) {
141
+ const targetChatId = chatId || this.lastChatId;
142
+ if (!targetChatId) throw new Error('No chatId provided and no previous chat to continue');
143
+
144
+ return this.runJob(message, {
145
+ ...options,
146
+ chatId: targetChatId,
147
+ includeHistory: options.includeHistory !== false
116
148
  });
117
-
118
- try {
119
- return JSON.parse(response)[0].createdChatId;
120
- } catch (error) {
121
- if (fs.existsSync(this.secretsPath)) {
122
- fs.unlinkSync(this.secretsPath);
123
- }
124
- throw new Error('Authentication failed.');
125
- }
126
149
  }
127
150
 
128
- makeRequest(url, payload) {
151
+ async uploadFile(filePath, options = {}) {
152
+ const apiKey = await this.getApiKey();
153
+ if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
154
+
155
+ const fileContent = fs.readFileSync(filePath);
156
+ const ext = path.extname(filePath);
157
+ const fileName = options.fileName || `file-${Date.now()}-${Math.random().toString(36).substring(7)}${ext}`;
158
+ const fileType = options.fileType || this.getMimeType(filePath);
159
+
160
+ const presignedResponse = await this.request('https://kb.openkbs.com/', {
161
+ apiKey,
162
+ kbId: this.settings.kbId,
163
+ namespace: 'files',
164
+ presignedOperation: 'putObject',
165
+ action: 'createPresignedURL',
166
+ fileName,
167
+ fileType
168
+ }, { Origin: `https://${this.settings.kbId}.apps.openkbs.com` });
169
+
170
+ const presignedUrl = presignedResponse.presignedUrl || presignedResponse;
171
+
172
+ await this.requestRaw(presignedUrl, fileContent, {
173
+ 'Content-Type': fileType,
174
+ 'Content-Length': fileContent.length
175
+ }, 'PUT');
176
+
177
+ return {
178
+ fileName,
179
+ uploaded: true,
180
+ url: `https://file.openkbs.com/files/${this.settings.kbId}/${fileName}`
181
+ };
182
+ }
183
+
184
+ getMimeType(filePath) {
185
+ const ext = path.extname(filePath).toLowerCase();
186
+ const mimeTypes = {
187
+ '.pdf': 'application/pdf',
188
+ '.txt': 'text/plain',
189
+ '.json': 'application/json',
190
+ '.jpg': 'image/jpeg',
191
+ '.jpeg': 'image/jpeg',
192
+ '.png': 'image/png',
193
+ '.gif': 'image/gif',
194
+ '.doc': 'application/msword',
195
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
196
+ '.xls': 'application/vnd.ms-excel',
197
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
198
+ '.csv': 'text/csv',
199
+ '.html': 'text/html',
200
+ '.xml': 'application/xml',
201
+ '.zip': 'application/zip'
202
+ };
203
+ return mimeTypes[ext] || 'application/octet-stream';
204
+ }
205
+
206
+ // Unified HTTP request helper
207
+ async request(url, payload, headers = {}, method = 'POST') {
129
208
  return new Promise((resolve, reject) => {
130
- const { hostname, pathname } = new URL(url);
209
+ const { hostname, pathname, search } = new URL(url);
131
210
  const req = https.request({
132
- hostname,
133
- path: pathname,
134
- method: 'POST',
135
- headers: { 'Content-Type': 'application/json' }
211
+ hostname,
212
+ path: pathname + (search || ''),
213
+ method,
214
+ headers: { 'Content-Type': 'application/json', ...headers }
136
215
  }, res => {
137
216
  let data = '';
138
217
  res.on('data', chunk => data += chunk);
139
- res.on('end', () => resolve(data));
218
+ res.on('end', () => {
219
+ try {
220
+ const result = JSON.parse(data);
221
+ if (res.statusCode === 401) {
222
+ if (fs.existsSync(this.secretsPath)) fs.unlinkSync(this.secretsPath);
223
+ reject(new Error('Authentication failed. Please run "node scripts/run_job.js init" to reconfigure.'));
224
+ } else if (res.statusCode !== 200) {
225
+ reject(new Error(`Request failed with status ${res.statusCode}: ${data}`));
226
+ } else {
227
+ resolve(result);
228
+ }
229
+ } catch (error) {
230
+ reject(new Error(`Failed to parse response: ${error.message}`));
231
+ }
232
+ });
140
233
  }).on('error', reject);
141
234
 
142
- req.write(JSON.stringify(payload));
235
+ req.write(typeof payload === 'string' ? payload : JSON.stringify(payload));
143
236
  req.end();
144
237
  });
145
238
  }
146
239
 
147
- async pollForMessages(chatId, app) {
148
- const payload = {
149
- ...app,
150
- action: 'getChatMessages',
151
- chatId,
152
- decryptContent: true
153
- };
154
-
155
- return new Promise((resolve) => {
156
- const interval = setInterval(() => {
157
- this.makeRequest('https://chat.openkbs.com/', payload)
158
- .then(jsonString => {
159
- const messages = JSON.parse(jsonString)[0].data.messages;
160
- for (const message of messages) {
161
- if (message.role === 'system' &&
162
- /{"type"\s*:\s*"(JOB_COMPLETED|JOB_FAILED)".*?}/s.test(message.content)) {
163
-
164
- const result = JSON.parse(message.content)?.data?.find(item =>
165
- item.type === 'JOB_COMPLETED' || item.type === 'JOB_FAILED'
166
- );
167
- clearInterval(interval);
168
- resolve(result);
169
- return;
170
- }
171
- }
172
- })
173
- .catch(console.error);
174
- }, 1000);
240
+ // Raw request for binary data
241
+ async requestRaw(url, data, headers = {}, method = 'PUT') {
242
+ return new Promise((resolve, reject) => {
243
+ const { hostname, pathname, search } = new URL(url);
244
+ const req = https.request({
245
+ hostname,
246
+ path: pathname + (search || ''),
247
+ method,
248
+ headers
249
+ }, res => {
250
+ if (res.statusCode === 200 || res.statusCode === 204) {
251
+ resolve();
252
+ } else {
253
+ let responseData = '';
254
+ res.on('data', chunk => responseData += chunk);
255
+ res.on('end', () => reject(new Error(`Request failed with status ${res.statusCode}: ${responseData}`)));
256
+ }
257
+ }).on('error', reject);
258
+
259
+ req.write(data);
260
+ req.end();
175
261
  });
176
262
  }
177
263
  }
@@ -13,7 +13,7 @@ export const backendHandler = async (event) => {
13
13
  return acc;
14
14
  }, []);
15
15
 
16
- // Avoid unnecessary LLM reactions if job is finished
16
+ // IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
17
17
  const isJobFinished = /"JOB_COMPLETED"|"JOB_FAILED"/.test(lastMessage.content);
18
18
 
19
19
  const meta = {
package/version.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.51",
3
- "releaseDate": "2025-08-01",
4
- "releaseNotes": "OpenKBS CLI version 0.0.51"
2
+ "version": "0.0.53",
3
+ "releaseDate": "2025-09-25",
4
+ "releaseNotes": "OpenKBS CLI version 0.0.53"
5
5
  }