blun-king-cli 9.1.470 → 9.1.472

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.
@@ -1,4 +1,5 @@
1
1
  const AGENT_TURNS_PATH = '/v1/agent/turns';
2
+ const AGENT_SESSIONS_PATH = '/v1/agent/sessions';
2
3
  const PUBLIC_MODEL_ID = 'king';
3
4
  const PUBLIC_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
4
5
 
@@ -70,9 +71,26 @@ function decodeRequestId(value) {
70
71
  return requirePublicId(decoded, 'request ID');
71
72
  }
72
73
 
74
+ function decodeSessionId(value) {
75
+ let decoded;
76
+ try {
77
+ decoded = decodeURIComponent(value);
78
+ } catch {
79
+ throw adapterError('AGENT_API_INVALID_ID', 'session ID is not valid URL encoding');
80
+ }
81
+ return requirePublicId(decoded, 'session ID');
82
+ }
83
+
73
84
  function routeFor(method, rawPath) {
74
85
  const path = String(rawPath ?? '').split('?', 1)[0];
75
86
  if (method === 'POST' && path === AGENT_TURNS_PATH) return { kind: 'start' };
87
+ if (path.startsWith(`${AGENT_SESSIONS_PATH}/`)) {
88
+ const tail = path.slice(AGENT_SESSIONS_PATH.length + 1);
89
+ if (method === 'DELETE' && !tail.includes('/')) {
90
+ return { kind: 'closeSession', sessionId: decodeSessionId(tail) };
91
+ }
92
+ return { kind: 'unknown' };
93
+ }
76
94
  if (!path.startsWith(`${AGENT_TURNS_PATH}/`)) {
77
95
  if (path.startsWith('/v1/agent/')) return { kind: 'unknown' };
78
96
  return null;
@@ -207,8 +225,19 @@ function createAgentApiHttpAdapter(options = {}) {
207
225
  if (typeof options.resolveSession !== 'function') {
208
226
  throw new TypeError('resolveSession must be a function');
209
227
  }
228
+ if (typeof options.closeSession !== 'function') {
229
+ throw new TypeError('closeSession must be a function');
230
+ }
210
231
  const owners = new Map();
211
232
 
233
+ function hasActiveOwner(principalId, sessionId) {
234
+ const prefix = `${sessionId}\0`;
235
+ for (const [key, scopeOwners] of owners) {
236
+ if (key.startsWith(prefix) && scopeOwners.has(principalId)) return true;
237
+ }
238
+ return false;
239
+ }
240
+
212
241
  function requireOwner(principalId, sessionId, requestId) {
213
242
  const key = scopeKey(sessionId, requestId);
214
243
  const scopeOwners = owners.get(key);
@@ -313,6 +342,31 @@ function createAgentApiHttpAdapter(options = {}) {
313
342
  });
314
343
  }
315
344
 
345
+ async function handleCloseSession(principalId, sessionId) {
346
+ if (hasActiveOwner(principalId, sessionId)) {
347
+ throw adapterError(
348
+ 'AGENT_API_SESSION_BUSY',
349
+ 'Agent session has an active turn.',
350
+ 409,
351
+ );
352
+ }
353
+ const closed = await options.closeSession({ principalId, sessionId });
354
+ if (closed !== true) {
355
+ throw adapterError(
356
+ 'AGENT_API_SESSION_NOT_FOUND',
357
+ 'Agent session was not found.',
358
+ 404,
359
+ );
360
+ }
361
+ return jsonResponse(200, {
362
+ id: `agent-session-${sessionId}`,
363
+ object: 'agent.session.closed',
364
+ model: PUBLIC_MODEL_ID,
365
+ session_id: sessionId,
366
+ closed: true,
367
+ });
368
+ }
369
+
316
370
  async function handleRequest(input) {
317
371
  if (input === null || typeof input !== 'object') return null;
318
372
  const method = String(input.method ?? '').toUpperCase();
@@ -331,6 +385,9 @@ function createAgentApiHttpAdapter(options = {}) {
331
385
  }
332
386
  const principalId = requireTrustedId(input.principalId, 'principalId');
333
387
  if (route.kind === 'start') return await handleStart(input, principalId);
388
+ if (route.kind === 'closeSession') {
389
+ return await handleCloseSession(principalId, route.sessionId);
390
+ }
334
391
  if (route.kind === 'continue') {
335
392
  return await handleContinue(input, principalId, route.requestId);
336
393
  }
@@ -344,6 +401,7 @@ function createAgentApiHttpAdapter(options = {}) {
344
401
  }
345
402
 
346
403
  export {
404
+ AGENT_SESSIONS_PATH,
347
405
  AGENT_TURNS_PATH,
348
406
  AgentApiHttpAdapterError,
349
407
  PUBLIC_MODEL_ID,
@@ -0,0 +1,199 @@
1
+ import path from 'node:path';
2
+
3
+ import { createAgentApiHttpAdapter } from './agent-api-http-adapter.mjs';
4
+ import { createAgentApiPrivateHttpServer } from './agent-api-private-http-server.mjs';
5
+ import { createAgentApiRuntime } from './agent-api-runtime.mjs';
6
+ import { createAgentApiSessionRegistry } from './agent-api-session-registry.mjs';
7
+ import { createAgentApiTurnController } from './agent-api-turn-controller.mjs';
8
+
9
+ const MIN_SECRET_BYTES = 32;
10
+
11
+ function requireSecret(value, label) {
12
+ if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') < MIN_SECRET_BYTES) {
13
+ throw new TypeError(`${label} must contain at least ${MIN_SECRET_BYTES} bytes`);
14
+ }
15
+ return value;
16
+ }
17
+
18
+ function requireAbsoluteRoot(value) {
19
+ if (typeof value !== 'string' || !path.isAbsolute(value)) {
20
+ throw new TypeError('rootDir must be an absolute path');
21
+ }
22
+ return path.resolve(value);
23
+ }
24
+
25
+ function requireObject(value, label) {
26
+ if (value === undefined) return {};
27
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
28
+ throw new TypeError(`${label} must be an object`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function cloneHarnessConfig(value) {
34
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
35
+ throw new TypeError('harnessConfig must be an object');
36
+ }
37
+ let config;
38
+ try {
39
+ config = structuredClone(value);
40
+ } catch {
41
+ throw new TypeError('harnessConfig must be structured-cloneable');
42
+ }
43
+ if (config.defaultModel !== 'king') {
44
+ throw new TypeError('harnessConfig.defaultModel must be king');
45
+ }
46
+ if (config.models === null
47
+ || typeof config.models !== 'object'
48
+ || Array.isArray(config.models)
49
+ || config.models.king === null
50
+ || typeof config.models.king !== 'object'
51
+ || Array.isArray(config.models.king)) {
52
+ throw new TypeError('harnessConfig.models.king must be an object');
53
+ }
54
+ const provider = config.models.king.provider;
55
+ if (typeof provider !== 'string'
56
+ || provider.length === 0
57
+ || config.providers === null
58
+ || typeof config.providers !== 'object'
59
+ || Array.isArray(config.providers)
60
+ || !Object.hasOwn(config.providers, provider)) {
61
+ throw new TypeError('harnessConfig must define the provider used by models.king');
62
+ }
63
+ return config;
64
+ }
65
+
66
+ async function createBundledHarness(input) {
67
+ const { createBlunHarness } = await import('../blun.mjs');
68
+ return createBlunHarness(input);
69
+ }
70
+
71
+ function resolveHarnessPreparation(options) {
72
+ const hasConfig = options.harnessConfig !== undefined;
73
+ const hasPrepare = options.prepareHarness !== undefined;
74
+ if (hasConfig && hasPrepare) {
75
+ throw new TypeError('only one of harnessConfig or prepareHarness may be provided');
76
+ }
77
+ if (!hasConfig && !hasPrepare) {
78
+ throw new TypeError('harnessConfig or prepareHarness is required');
79
+ }
80
+ if (hasPrepare) {
81
+ if (typeof options.prepareHarness !== 'function') {
82
+ throw new TypeError('prepareHarness must be a function');
83
+ }
84
+ return options.prepareHarness;
85
+ }
86
+
87
+ const config = cloneHarnessConfig(options.harnessConfig);
88
+ return async ({ harness }) => {
89
+ if (typeof harness?.setConfig !== 'function') {
90
+ throw new TypeError('Agent API harness must provide setConfig');
91
+ }
92
+ await harness.setConfig(structuredClone(config));
93
+ };
94
+ }
95
+
96
+ function createAgentApiServiceHost(options = {}) {
97
+ const rootDir = requireAbsoluteRoot(options.rootDir);
98
+ const internalSecret = requireSecret(options.internalSecret, 'internalSecret');
99
+ const isolationSecret = requireSecret(options.isolationSecret, 'isolationSecret');
100
+ if (internalSecret === isolationSecret) {
101
+ throw new TypeError('internalSecret and isolationSecret must be different');
102
+ }
103
+ if (options.createHarness !== undefined && typeof options.createHarness !== 'function') {
104
+ throw new TypeError('createHarness must be a function');
105
+ }
106
+
107
+ const runtimeOptions = requireObject(options.runtimeOptions, 'runtimeOptions');
108
+ const controllerOptions = requireObject(options.controllerOptions, 'controllerOptions');
109
+ const prepareHarness = resolveHarnessPreparation(options);
110
+ const runtime = createAgentApiRuntime(runtimeOptions);
111
+ const controller = createAgentApiTurnController({
112
+ ...controllerOptions,
113
+ runtime,
114
+ publicModel: 'king',
115
+ });
116
+ const sessionRegistry = createAgentApiSessionRegistry({
117
+ rootDir,
118
+ isolationSecret,
119
+ createHarness: options.createHarness ?? createBundledHarness,
120
+ prepareHarness,
121
+ sessionOptions: options.sessionOptions,
122
+ maxPrincipals: options.maxPrincipals,
123
+ maxSessionsPerPrincipal: options.maxSessionsPerPrincipal,
124
+ });
125
+ const adapter = createAgentApiHttpAdapter({
126
+ controller,
127
+ resolveSession: sessionRegistry.resolveSession,
128
+ closeSession: sessionRegistry.closeSession,
129
+ });
130
+ const privateServer = createAgentApiPrivateHttpServer({
131
+ adapter,
132
+ internalSecret,
133
+ maxBodyBytes: options.maxBodyBytes,
134
+ });
135
+
136
+ let state = 'idle';
137
+ let listenPromise;
138
+ let closePromise;
139
+
140
+ async function listen(input = {}) {
141
+ if (state === 'closed' || state === 'closing') {
142
+ throw new Error('Agent API service host is closed');
143
+ }
144
+ if (state !== 'idle') {
145
+ throw new Error('Agent API service host is already listening');
146
+ }
147
+ state = 'starting';
148
+ try {
149
+ listenPromise = privateServer.listen(input);
150
+ const address = await listenPromise;
151
+ if (state === 'closing' || state === 'closed') {
152
+ await privateServer.close();
153
+ throw new Error('Agent API service host is closed');
154
+ }
155
+ state = 'listening';
156
+ return address;
157
+ } catch (error) {
158
+ if (state === 'starting') state = 'idle';
159
+ throw error;
160
+ } finally {
161
+ listenPromise = undefined;
162
+ }
163
+ }
164
+
165
+ function close() {
166
+ if (closePromise !== undefined) return closePromise;
167
+ state = 'closing';
168
+ closePromise = (async () => {
169
+ let firstError;
170
+ if (listenPromise !== undefined) {
171
+ try {
172
+ await listenPromise;
173
+ } catch {
174
+ // The listen failure remains with its caller; cleanup continues here.
175
+ }
176
+ }
177
+ try {
178
+ await privateServer.close();
179
+ } catch (error) {
180
+ firstError = error;
181
+ }
182
+ try {
183
+ await sessionRegistry.close();
184
+ } catch (error) {
185
+ firstError ??= error;
186
+ }
187
+ state = 'closed';
188
+ if (firstError !== undefined) throw firstError;
189
+ })();
190
+ return closePromise;
191
+ }
192
+
193
+ return Object.freeze({ close, listen });
194
+ }
195
+
196
+ export {
197
+ MIN_SECRET_BYTES,
198
+ createAgentApiServiceHost,
199
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.470",
3
+ "version": "9.1.472",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {