@juspay/neurolink 9.85.1 → 9.86.1
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +370 -362
- package/dist/context/contextCompactor.js +16 -2
- package/dist/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/core/conversationMemoryManager.js +13 -2
- package/dist/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/context/contextCompactor.js +16 -2
- package/dist/lib/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/lib/core/conversationMemoryManager.js +13 -2
- package/dist/lib/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/neurolink.d.ts +31 -6
- package/dist/lib/neurolink.js +163 -33
- package/dist/lib/proxy/accountQuota.d.ts +5 -0
- package/dist/lib/proxy/accountQuota.js +8 -1
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +22 -6
- package/dist/lib/server/routes/claudeProxyRoutes.js +52 -8
- package/dist/lib/skills/skillMatcher.d.ts +33 -4
- package/dist/lib/skills/skillMatcher.js +81 -6
- package/dist/lib/skills/skillSessionTracker.d.ts +52 -0
- package/dist/lib/skills/skillSessionTracker.js +150 -0
- package/dist/lib/skills/skillStoreRedis.d.ts +8 -0
- package/dist/lib/skills/skillStoreRedis.js +18 -2
- package/dist/lib/skills/skillStoreS3.d.ts +17 -2
- package/dist/lib/skills/skillStoreS3.js +78 -6
- package/dist/lib/skills/skillStores.d.ts +16 -2
- package/dist/lib/skills/skillStores.js +94 -5
- package/dist/lib/skills/skillTools.d.ts +26 -10
- package/dist/lib/skills/skillTools.js +190 -79
- package/dist/lib/skills/skillsManager.d.ts +25 -1
- package/dist/lib/skills/skillsManager.js +46 -4
- package/dist/lib/types/config.d.ts +5 -5
- package/dist/lib/types/conversation.d.ts +27 -0
- package/dist/lib/types/skills.d.ts +145 -14
- package/dist/lib/types/skills.js +3 -3
- package/dist/lib/utils/conversationMemory.d.ts +1 -1
- package/dist/lib/utils/conversationMemory.js +15 -2
- package/dist/neurolink.d.ts +31 -6
- package/dist/neurolink.js +163 -33
- package/dist/proxy/accountQuota.d.ts +5 -0
- package/dist/proxy/accountQuota.js +8 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +22 -6
- package/dist/server/routes/claudeProxyRoutes.js +52 -8
- package/dist/skills/skillMatcher.d.ts +33 -4
- package/dist/skills/skillMatcher.js +81 -6
- package/dist/skills/skillSessionTracker.d.ts +52 -0
- package/dist/skills/skillSessionTracker.js +149 -0
- package/dist/skills/skillStoreRedis.d.ts +8 -0
- package/dist/skills/skillStoreRedis.js +18 -2
- package/dist/skills/skillStoreS3.d.ts +17 -2
- package/dist/skills/skillStoreS3.js +78 -6
- package/dist/skills/skillStores.d.ts +16 -2
- package/dist/skills/skillStores.js +94 -5
- package/dist/skills/skillTools.d.ts +26 -10
- package/dist/skills/skillTools.js +190 -79
- package/dist/skills/skillsManager.d.ts +25 -1
- package/dist/skills/skillsManager.js +46 -4
- package/dist/types/config.d.ts +5 -5
- package/dist/types/conversation.d.ts +27 -0
- package/dist/types/skills.d.ts +145 -14
- package/dist/types/skills.js +3 -3
- package/dist/utils/conversationMemory.d.ts +1 -1
- package/dist/utils/conversationMemory.js +15 -2
- package/package.json +1 -1
package/dist/types/skills.d.ts
CHANGED
|
@@ -6,15 +6,28 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Architecture mirrors the Hippocampus memory subsystem: a `skills` config
|
|
8
8
|
* on the NeuroLink constructor lazily initializes a SkillsManager, which
|
|
9
|
-
* auto-registers built-in tools (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* auto-registers built-in tools (list_skills, plus gated mutation tools)
|
|
10
|
+
* and augments each generate()/stream() call with the discovery listing
|
|
11
|
+
* plus per-call use_skill / read_skill_resource tools.
|
|
12
12
|
*
|
|
13
13
|
* Naming: every exported type carries a `Skill` prefix to satisfy the
|
|
14
14
|
* `unique-type-names` ESLint rule.
|
|
15
15
|
*/
|
|
16
|
+
import type { ChatMessage } from "./conversation.js";
|
|
16
17
|
/** Visibility of a skill: available everywhere, or only in specific scopes. */
|
|
17
18
|
export type SkillScopeKind = "global" | "scoped";
|
|
19
|
+
/**
|
|
20
|
+
* Reference to an auxiliary file bundled with a skill (progressive
|
|
21
|
+
* disclosure level 3). Resources are read into context on demand via the
|
|
22
|
+
* read_skill_resource tool — a skill's SKILL.md should stay lean and point
|
|
23
|
+
* to resources for rarely-needed detail.
|
|
24
|
+
*/
|
|
25
|
+
export type SkillResourceRef = {
|
|
26
|
+
/** Path relative to the skill's directory, e.g. "references/edge-cases.md". */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Size in bytes when known (listing hint only). */
|
|
29
|
+
size?: number;
|
|
30
|
+
};
|
|
18
31
|
/** Lifecycle status. Deletes are soft — deprecated skills stay in storage. */
|
|
19
32
|
export type SkillLifecycleStatus = "active" | "deprecated";
|
|
20
33
|
/**
|
|
@@ -47,6 +60,12 @@ export type SkillDefinition = {
|
|
|
47
60
|
createdAt?: string;
|
|
48
61
|
/** ISO timestamp of last update. */
|
|
49
62
|
updatedAt?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Auxiliary files bundled with the skill, readable on demand through
|
|
65
|
+
* read_skill_resource. Populated by stores that support resources
|
|
66
|
+
* (directory-layout filesystem skills, S3, Redis).
|
|
67
|
+
*/
|
|
68
|
+
resources?: SkillResourceRef[];
|
|
50
69
|
/** Free-form host metadata (audit fields, approval references, …). */
|
|
51
70
|
metadata?: Record<string, unknown>;
|
|
52
71
|
};
|
|
@@ -69,12 +88,24 @@ export type SkillStore = {
|
|
|
69
88
|
index(): Promise<SkillIndexItem[]>;
|
|
70
89
|
/** Optional: drop any internal caches (called after mutations). */
|
|
71
90
|
invalidate?(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Optional: fetch an auxiliary resource file bundled with a skill.
|
|
93
|
+
* `resourcePath` is relative to the skill (e.g. "references/forms.md").
|
|
94
|
+
* Null when the skill or resource is absent. Stores without resource
|
|
95
|
+
* support simply omit this method.
|
|
96
|
+
*/
|
|
97
|
+
getResource?(id: string, resourcePath: string): Promise<string | null>;
|
|
72
98
|
};
|
|
73
99
|
/** In-process store, optionally seeded. Good for tests and embedded use. */
|
|
74
100
|
export type SkillMemoryStorageConfig = {
|
|
75
101
|
type: "memory";
|
|
76
102
|
/** Initial skills to seed the store with. */
|
|
77
103
|
skills?: SkillDefinition[];
|
|
104
|
+
/**
|
|
105
|
+
* Resource file contents keyed by skill id → relative path.
|
|
106
|
+
* E.g. `{ "my-skill": { "references/forms.md": "..." } }`.
|
|
107
|
+
*/
|
|
108
|
+
resources?: Record<string, Record<string, string>>;
|
|
78
109
|
};
|
|
79
110
|
/**
|
|
80
111
|
* Directory-backed store. Reads three layouts:
|
|
@@ -154,6 +185,15 @@ export type SkillS3IndexDocument = {
|
|
|
154
185
|
lastUpdated: string;
|
|
155
186
|
skills: SkillIndexItem[];
|
|
156
187
|
};
|
|
188
|
+
/** Result of a conditional (ETag) object read. */
|
|
189
|
+
export type SkillS3ConditionalGetResult = {
|
|
190
|
+
/** Object body; null when the key is absent. */
|
|
191
|
+
body: string | null;
|
|
192
|
+
/** ETag of the returned body, for the next conditional read. */
|
|
193
|
+
etag?: string;
|
|
194
|
+
/** True when the object is unchanged since the supplied ETag (no body). */
|
|
195
|
+
notModified?: boolean;
|
|
196
|
+
};
|
|
157
197
|
/**
|
|
158
198
|
* Minimal object-storage operations the S3 skill store runs on. The
|
|
159
199
|
* default implementation is created lazily from @aws-sdk/client-s3;
|
|
@@ -166,9 +206,15 @@ export type SkillS3ObjectOps = {
|
|
|
166
206
|
deleteObject(key: string): Promise<void>;
|
|
167
207
|
/** List all object keys under a prefix (paginated internally). */
|
|
168
208
|
listKeys(prefix: string): Promise<string[]>;
|
|
209
|
+
/**
|
|
210
|
+
* Optional: ETag-conditional read (If-None-Match). Used for index.json
|
|
211
|
+
* refreshes so an unchanged index costs a 304 instead of a full download.
|
|
212
|
+
* Ops without it fall back to plain getObject.
|
|
213
|
+
*/
|
|
214
|
+
getObjectConditional?(key: string, etag?: string): Promise<SkillS3ConditionalGetResult>;
|
|
169
215
|
};
|
|
170
216
|
export type SkillsStorageConfig = SkillMemoryStorageConfig | SkillFilesystemStorageConfig | SkillS3StorageConfig | SkillRedisStorageConfig | SkillCustomStorageConfig;
|
|
171
|
-
/** Query accepted by SkillsManager.search()
|
|
217
|
+
/** Query accepted by SkillsManager.search() (programmatic + CLI search). */
|
|
172
218
|
export type SkillSearchQuery = {
|
|
173
219
|
/** Keyword matched (case-insensitive substring) against name, displayName, and description. */
|
|
174
220
|
query?: string;
|
|
@@ -239,14 +285,38 @@ export type SkillsConfig = {
|
|
|
239
285
|
/** Persistence backend. Default: `{ type: "memory" }`. */
|
|
240
286
|
storage?: SkillsStorageConfig;
|
|
241
287
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
288
|
+
* Where the skills listing (names + descriptions, never instructions)
|
|
289
|
+
* surfaces for model-driven discovery:
|
|
290
|
+
* - "tool" (default): an `<available_skills>` block embedded in the
|
|
291
|
+
* use_skill tool description — the Claude Code pattern. Keeps the
|
|
292
|
+
* host's system prompt untouched and the listing cache-stable.
|
|
293
|
+
* - "system-prompt": a "## Available Skills" index appended to the
|
|
294
|
+
* system prompt instead.
|
|
295
|
+
* - "none": no listing anywhere; discovery only via list_skills.
|
|
296
|
+
*/
|
|
297
|
+
discovery?: SkillDiscoveryMode;
|
|
298
|
+
/**
|
|
299
|
+
* Character budget for the "tool" discovery listing. When the full
|
|
300
|
+
* listing exceeds it, every description is shortened uniformly (first
|
|
301
|
+
* sentence, then a hard cap) so the render stays a pure function of the
|
|
302
|
+
* index — byte-stable across calls; names are never dropped.
|
|
303
|
+
* Default: 15000.
|
|
245
304
|
*/
|
|
246
|
-
|
|
305
|
+
listingBudgetChars?: number;
|
|
306
|
+
/**
|
|
307
|
+
* Pin activated skill instructions into session history so later turns
|
|
308
|
+
* replay them verbatim (byte-stable, provider-cacheable) instead of
|
|
309
|
+
* re-fetching the skill. Requires conversation memory + a sessionId on
|
|
310
|
+
* the call. Default: true.
|
|
311
|
+
*/
|
|
312
|
+
sessionPersistence?: boolean;
|
|
247
313
|
/** Maximum skills hydrated (with instructions) per search. Default: 5. */
|
|
248
314
|
maxMatches?: number;
|
|
249
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Maximum entries rendered by the "system-prompt" discovery mode before
|
|
317
|
+
* truncation. Default: 50. The "tool" mode is bounded by
|
|
318
|
+
* listingBudgetChars instead and never drops entries.
|
|
319
|
+
*/
|
|
250
320
|
promptIndexMaxItems?: number;
|
|
251
321
|
/** Index cache TTL in milliseconds. Default: 30000. 0 disables caching. */
|
|
252
322
|
indexCacheTtlMs?: number;
|
|
@@ -265,6 +335,30 @@ export type SkillsConfig = {
|
|
|
265
335
|
*/
|
|
266
336
|
onMutationRequest?: (action: SkillMutationAction) => Promise<SkillMutationDecision>;
|
|
267
337
|
};
|
|
338
|
+
/** Where the skills discovery listing surfaces. See SkillsConfig.discovery. */
|
|
339
|
+
export type SkillDiscoveryMode = "tool" | "system-prompt" | "none";
|
|
340
|
+
/**
|
|
341
|
+
* One activated skill in a session: which skill, at which version, when.
|
|
342
|
+
* Sessions pin the version active at activation time — a mid-session skill
|
|
343
|
+
* update never mutates instructions the model has already loaded.
|
|
344
|
+
*/
|
|
345
|
+
export type SkillActivationRecord = {
|
|
346
|
+
skillId: string;
|
|
347
|
+
name: string;
|
|
348
|
+
version: number;
|
|
349
|
+
/** ISO timestamp of activation. */
|
|
350
|
+
activatedAt: string;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Structural view of the per-session activation tracker consumed by the
|
|
354
|
+
* skill tools factory.
|
|
355
|
+
*/
|
|
356
|
+
export type SkillSessionStateLike = {
|
|
357
|
+
isActive: (sessionId: string, skillId: string, name: string) => boolean;
|
|
358
|
+
getActivation: (sessionId: string, skillId: string, name?: string) => SkillActivationRecord | undefined;
|
|
359
|
+
recordActivation: (sessionId: string, skill: SkillDefinition) => ChatMessage;
|
|
360
|
+
hydrate: (sessionId: string, storedMessages: ChatMessage[]) => void;
|
|
361
|
+
};
|
|
268
362
|
/**
|
|
269
363
|
* Structural view of SkillsManager consumed by the skill tools factory —
|
|
270
364
|
* keeps skillTools.ts decoupled from the concrete manager class.
|
|
@@ -272,8 +366,38 @@ export type SkillsConfig = {
|
|
|
272
366
|
export type SkillsManagerLike = {
|
|
273
367
|
search: (query: SkillSearchQuery) => Promise<SkillDefinition[]>;
|
|
274
368
|
list: (scopeId?: string) => Promise<SkillIndexItem[]>;
|
|
369
|
+
get: (idOrName: string) => Promise<SkillDefinition | null>;
|
|
370
|
+
getResource: (idOrName: string, resourcePath: string) => Promise<string | null>;
|
|
371
|
+
sessions: SkillSessionStateLike;
|
|
275
372
|
requestMutation: (action: SkillMutationAction) => Promise<SkillMutationResult>;
|
|
276
373
|
};
|
|
374
|
+
/**
|
|
375
|
+
* Per-call context bound into the use_skill / read_skill_resource tools at
|
|
376
|
+
* injection time (prepareGenerate/prepareStream). The sessionId is captured
|
|
377
|
+
* by closure so activation state is tracked without relying on runtime tool
|
|
378
|
+
* context plumbing.
|
|
379
|
+
*/
|
|
380
|
+
export type SkillCallToolsContext = {
|
|
381
|
+
/** Session the call belongs to; absent → activation state is per-turn only. */
|
|
382
|
+
sessionId?: string;
|
|
383
|
+
/** Scope filter applied when resolving skills for this call. */
|
|
384
|
+
scopeId?: string;
|
|
385
|
+
/**
|
|
386
|
+
* Pin activated instructions into session history after the turn.
|
|
387
|
+
* Mirrors SkillsConfig.sessionPersistence resolved for this call.
|
|
388
|
+
*/
|
|
389
|
+
sessionPersistence: boolean;
|
|
390
|
+
/** Discovery mode resolved for this call — shapes the use_skill description. */
|
|
391
|
+
discovery: SkillDiscoveryMode;
|
|
392
|
+
/** Rendered `<available_skills>` block for "tool" discovery; null when empty. */
|
|
393
|
+
listing?: string | null;
|
|
394
|
+
/**
|
|
395
|
+
* Stored session history loader used to hydrate activation state before
|
|
396
|
+
* every dedup check (restart/multi-instance/failed-persistence safety).
|
|
397
|
+
* Invoked once per use_skill / read_skill_resource attempt.
|
|
398
|
+
*/
|
|
399
|
+
getStoredMessages?: (sessionId: string) => Promise<ChatMessage[]>;
|
|
400
|
+
};
|
|
277
401
|
/** Options for the createSkillTools factory. */
|
|
278
402
|
export type SkillToolsOptions = {
|
|
279
403
|
/** Include skill_create / skill_update / skill_delete. Default: false. */
|
|
@@ -285,12 +409,19 @@ export type SkillToolsOptions = {
|
|
|
285
409
|
* config (same precedence convention as per-call credentials).
|
|
286
410
|
*/
|
|
287
411
|
export type SkillsCallOptions = {
|
|
288
|
-
/** Master toggle for this call (
|
|
412
|
+
/** Master toggle for this call (listing + per-call tools). Default: true. */
|
|
289
413
|
enabled?: boolean;
|
|
290
|
-
/** Per-call override of SkillsConfig.
|
|
291
|
-
|
|
292
|
-
/** Scope filter for the
|
|
414
|
+
/** Per-call override of SkillsConfig.discovery. */
|
|
415
|
+
discovery?: SkillDiscoveryMode;
|
|
416
|
+
/** Scope filter for the listing and skill resolution on this call. Overrides defaultScopeId. */
|
|
293
417
|
scopeId?: string;
|
|
294
|
-
/** Restrict the
|
|
418
|
+
/** Restrict the listing to skills carrying at least one of these tags. */
|
|
295
419
|
tags?: string[];
|
|
420
|
+
/**
|
|
421
|
+
* Skill names to activate at the start of this call: their full
|
|
422
|
+
* instructions are injected up front (and pinned to the session when
|
|
423
|
+
* sessionPersistence is on), without waiting for the model to invoke
|
|
424
|
+
* use_skill. Already-active skills are skipped.
|
|
425
|
+
*/
|
|
426
|
+
preload?: string[];
|
|
296
427
|
};
|
package/dist/types/skills.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Architecture mirrors the Hippocampus memory subsystem: a `skills` config
|
|
8
8
|
* on the NeuroLink constructor lazily initializes a SkillsManager, which
|
|
9
|
-
* auto-registers built-in tools (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* auto-registers built-in tools (list_skills, plus gated mutation tools)
|
|
10
|
+
* and augments each generate()/stream() call with the discovery listing
|
|
11
|
+
* plus per-call use_skill / read_skill_resource tools.
|
|
12
12
|
*
|
|
13
13
|
* Naming: every exported type carries a `Skill` prefix to satisfy the
|
|
14
14
|
* `unique-type-names` ESLint rule.
|
|
@@ -28,7 +28,7 @@ export declare function getConversationMessages(conversationMemory: Conversation
|
|
|
28
28
|
* Store conversation turn for future context
|
|
29
29
|
* Saves user messages and AI responses for conversation memory
|
|
30
30
|
*/
|
|
31
|
-
export declare function storeConversationTurn(conversationMemory: ConversationMemoryManager | RedisConversationMemoryManager | null | undefined, originalOptions: TextGenerationOptions, result: TextGenerationResult, startTimeStamp?: Date | undefined, requestId?: string): Promise<void>;
|
|
31
|
+
export declare function storeConversationTurn(conversationMemory: ConversationMemoryManager | RedisConversationMemoryManager | null | undefined, originalOptions: TextGenerationOptions, result: TextGenerationResult, startTimeStamp?: Date | undefined, requestId?: string, skillMessages?: ChatMessage[]): Promise<void>;
|
|
32
32
|
/**
|
|
33
33
|
* Build context messages from pointer onwards (token-based memory)
|
|
34
34
|
* Returns summary message (if exists) + all messages after the summarized pointer
|
|
@@ -227,7 +227,7 @@ export async function getConversationMessages(conversationMemory, options) {
|
|
|
227
227
|
* Store conversation turn for future context
|
|
228
228
|
* Saves user messages and AI responses for conversation memory
|
|
229
229
|
*/
|
|
230
|
-
export async function storeConversationTurn(conversationMemory, originalOptions, result, startTimeStamp, requestId) {
|
|
230
|
+
export async function storeConversationTurn(conversationMemory, originalOptions, result, startTimeStamp, requestId, skillMessages) {
|
|
231
231
|
logger.debug("[conversationMemoryUtils] storeConversationTurn called", {
|
|
232
232
|
requestId,
|
|
233
233
|
hasMemory: !!conversationMemory,
|
|
@@ -369,6 +369,9 @@ export async function storeConversationTurn(conversationMemory, originalOptions,
|
|
|
369
369
|
enableSummarization: originalOptions.enableSummarization,
|
|
370
370
|
requestId,
|
|
371
371
|
events: toolActivityEvents,
|
|
372
|
+
...(skillMessages && skillMessages.length > 0
|
|
373
|
+
? { skillMessages }
|
|
374
|
+
: {}),
|
|
372
375
|
tokenUsage: result.usage
|
|
373
376
|
? {
|
|
374
377
|
inputTokens: result.usage.input,
|
|
@@ -450,6 +453,12 @@ export function buildContextFromPointer(session, requestId) {
|
|
|
450
453
|
return session.messages;
|
|
451
454
|
}
|
|
452
455
|
const messagesAfterPointer = session.messages.slice(pointerIndex + 1);
|
|
456
|
+
// Pinned skill instructions must survive summarization: any skill message
|
|
457
|
+
// that fell behind the pointer is re-included verbatim after the summary,
|
|
458
|
+
// so an activated skill keeps its full instructions for the whole session.
|
|
459
|
+
const pinnedSkillMessages = session.messages
|
|
460
|
+
.slice(0, pointerIndex + 1)
|
|
461
|
+
.filter((msg) => msg.metadata?.isSkill);
|
|
453
462
|
// Construct context: summary message + recent messages
|
|
454
463
|
const summaryMessage = {
|
|
455
464
|
id: `summary-${session.summarizedUpToMessageId}`,
|
|
@@ -468,7 +477,11 @@ export function buildContextFromPointer(session, requestId) {
|
|
|
468
477
|
totalMessages: session.messages.length,
|
|
469
478
|
summaryLength: session.summarizedMessage.length,
|
|
470
479
|
});
|
|
471
|
-
const contextMessages = [
|
|
480
|
+
const contextMessages = [
|
|
481
|
+
summaryMessage,
|
|
482
|
+
...pinnedSkillMessages,
|
|
483
|
+
...messagesAfterPointer,
|
|
484
|
+
];
|
|
472
485
|
// Log context built for LLM with structural metadata
|
|
473
486
|
const totalChars = contextMessages.reduce((sum, msg) => sum + msg.content.length, 0);
|
|
474
487
|
logger.info("[ConversationMemory] Context built for LLM", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.86.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
|
|
6
6
|
"author": {
|