@juspay/neurolink 9.84.2 → 9.85.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +410 -375
  3. package/dist/cli/factories/commandFactory.d.ts +17 -0
  4. package/dist/cli/factories/commandFactory.js +254 -0
  5. package/dist/cli/parser.js +2 -0
  6. package/dist/cli/utils/skillsFlags.d.ts +10 -0
  7. package/dist/cli/utils/skillsFlags.js +22 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.js +6 -0
  10. package/dist/lib/index.d.ts +5 -0
  11. package/dist/lib/index.js +6 -0
  12. package/dist/lib/neurolink.d.ts +28 -0
  13. package/dist/lib/neurolink.js +117 -0
  14. package/dist/lib/server/routes/agentRoutes.js +156 -1
  15. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +39 -1
  16. package/dist/lib/server/routes/claudeProxyRoutes.js +300 -41
  17. package/dist/lib/server/utils/validation.d.ts +32 -0
  18. package/dist/lib/server/utils/validation.js +18 -0
  19. package/dist/lib/session/globalSessionState.d.ts +10 -1
  20. package/dist/lib/session/globalSessionState.js +18 -0
  21. package/dist/lib/skills/skillMatcher.d.ts +20 -0
  22. package/dist/lib/skills/skillMatcher.js +80 -0
  23. package/dist/lib/skills/skillStoreRedis.d.ts +22 -0
  24. package/dist/lib/skills/skillStoreRedis.js +98 -0
  25. package/dist/lib/skills/skillStoreS3.d.ts +41 -0
  26. package/dist/lib/skills/skillStoreS3.js +233 -0
  27. package/dist/lib/skills/skillStores.d.ts +44 -0
  28. package/dist/lib/skills/skillStores.js +252 -0
  29. package/dist/lib/skills/skillTools.d.ts +19 -0
  30. package/dist/lib/skills/skillTools.js +340 -0
  31. package/dist/lib/skills/skillsManager.d.ts +54 -0
  32. package/dist/lib/skills/skillsManager.js +220 -0
  33. package/dist/lib/types/config.d.ts +10 -0
  34. package/dist/lib/types/generate.d.ts +8 -0
  35. package/dist/lib/types/index.d.ts +1 -0
  36. package/dist/lib/types/index.js +1 -0
  37. package/dist/lib/types/proxy.d.ts +30 -2
  38. package/dist/lib/types/skills.d.ts +296 -0
  39. package/dist/lib/types/skills.js +17 -0
  40. package/dist/lib/types/stream.d.ts +8 -0
  41. package/dist/neurolink.d.ts +28 -0
  42. package/dist/neurolink.js +117 -0
  43. package/dist/server/routes/agentRoutes.js +156 -1
  44. package/dist/server/routes/claudeProxyRoutes.d.ts +39 -1
  45. package/dist/server/routes/claudeProxyRoutes.js +300 -41
  46. package/dist/server/utils/validation.d.ts +32 -0
  47. package/dist/server/utils/validation.js +18 -0
  48. package/dist/session/globalSessionState.d.ts +10 -1
  49. package/dist/session/globalSessionState.js +18 -0
  50. package/dist/skills/skillMatcher.d.ts +20 -0
  51. package/dist/skills/skillMatcher.js +79 -0
  52. package/dist/skills/skillStoreRedis.d.ts +22 -0
  53. package/dist/skills/skillStoreRedis.js +97 -0
  54. package/dist/skills/skillStoreS3.d.ts +41 -0
  55. package/dist/skills/skillStoreS3.js +232 -0
  56. package/dist/skills/skillStores.d.ts +44 -0
  57. package/dist/skills/skillStores.js +251 -0
  58. package/dist/skills/skillTools.d.ts +19 -0
  59. package/dist/skills/skillTools.js +339 -0
  60. package/dist/skills/skillsManager.d.ts +54 -0
  61. package/dist/skills/skillsManager.js +219 -0
  62. package/dist/types/config.d.ts +10 -0
  63. package/dist/types/generate.d.ts +8 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +1 -0
  66. package/dist/types/proxy.d.ts +30 -2
  67. package/dist/types/skills.d.ts +296 -0
  68. package/dist/types/skills.js +16 -0
  69. package/dist/types/stream.d.ts +8 -0
  70. package/package.json +2 -1
@@ -0,0 +1,219 @@
1
+ /**
2
+ * SkillsManager — orchestrates the skill store, cached index, index-first
3
+ * search (hydrating instructions only for matches), prompt-index rendering,
4
+ * and the mutation gate.
5
+ *
6
+ * Read paths fail open (errors surface as empty results + a warn log);
7
+ * write paths fail closed (errors reject the mutation).
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+ import { logger } from "../utils/logger.js";
11
+ import { filterSkillIndex, formatSkillsPromptIndex } from "./skillMatcher.js";
12
+ import { createSkillStore } from "./skillStores.js";
13
+ const DEFAULT_MAX_MATCHES = 5;
14
+ const DEFAULT_PROMPT_INDEX_MAX_ITEMS = 50;
15
+ const DEFAULT_INDEX_CACHE_TTL_MS = 30_000;
16
+ export class SkillsManager {
17
+ config;
18
+ store;
19
+ cachedIndex = null;
20
+ cachedIndexAt = 0;
21
+ /** Serializes writes within this process — see requestMutation(). */
22
+ mutationQueue = Promise.resolve();
23
+ constructor(config) {
24
+ this.config = config;
25
+ this.store = createSkillStore(config.storage);
26
+ }
27
+ /** Cached index read. TTL 0 disables caching. */
28
+ async getIndex(forceRefresh = false) {
29
+ const ttl = this.config.indexCacheTtlMs ?? DEFAULT_INDEX_CACHE_TTL_MS;
30
+ const fresh = this.cachedIndex !== null &&
31
+ ttl > 0 &&
32
+ Date.now() - this.cachedIndexAt < ttl;
33
+ if (fresh && !forceRefresh && this.cachedIndex) {
34
+ return this.cachedIndex;
35
+ }
36
+ const index = await this.store.index();
37
+ this.cachedIndex = index;
38
+ this.cachedIndexAt = Date.now();
39
+ return index;
40
+ }
41
+ invalidateIndex() {
42
+ this.cachedIndex = null;
43
+ this.store.invalidate?.();
44
+ }
45
+ /**
46
+ * Index-first search: filter the cached index, hydrate only the matched
47
+ * entries (max `limit`) with instructions. Cost: one cached index read +
48
+ * N_matched store gets.
49
+ */
50
+ async search(query) {
51
+ const limit = query.limit ?? this.config.maxMatches ?? DEFAULT_MAX_MATCHES;
52
+ const scopeId = query.scopeId ?? this.config.defaultScopeId;
53
+ const index = await this.getIndex();
54
+ const matched = filterSkillIndex(index, {
55
+ ...query,
56
+ ...(scopeId !== undefined ? { scopeId } : {}),
57
+ limit,
58
+ });
59
+ const hydrated = await Promise.all(matched.map((item) => this.store.get(item.id)));
60
+ return hydrated.filter((s) => s !== null);
61
+ }
62
+ /** Index entries only — no instructions. For discovery/listing. */
63
+ async list(scopeId) {
64
+ const effectiveScopeId = scopeId ?? this.config.defaultScopeId;
65
+ const index = await this.getIndex();
66
+ return filterSkillIndex(index, {
67
+ ...(effectiveScopeId !== undefined ? { scopeId: effectiveScopeId } : {}),
68
+ });
69
+ }
70
+ /** Fetch one skill by id, falling back to name lookup. */
71
+ async get(idOrName) {
72
+ const byId = await this.store.get(idOrName);
73
+ if (byId) {
74
+ return byId;
75
+ }
76
+ const index = await this.getIndex();
77
+ // Prefer an active skill when resolving by name: a soft-deleted skill and a
78
+ // later same-named active skill can coexist in the index, so a bare
79
+ // name-find would resolve non-deterministically to the stale deprecated one
80
+ // across store backends. Fall back to any match only when no active skill
81
+ // carries the name.
82
+ const entry = index.find((item) => item.name === idOrName && (item.status ?? "active") === "active") ?? index.find((item) => item.name === idOrName);
83
+ return entry ? this.store.get(entry.id) : null;
84
+ }
85
+ /**
86
+ * Render the system-prompt skills index for one call, or null when
87
+ * nothing is visible. Never includes instructions.
88
+ */
89
+ async buildPromptIndex(options) {
90
+ let items = await this.list(options?.scopeId);
91
+ if (options?.tags && options.tags.length > 0) {
92
+ const wanted = options.tags.map((t) => t.toLowerCase());
93
+ items = items.filter((item) => (item.tags ?? []).some((t) => wanted.includes(t.toLowerCase())));
94
+ }
95
+ return formatSkillsPromptIndex(items, this.config.promptIndexMaxItems ?? DEFAULT_PROMPT_INDEX_MAX_ITEMS);
96
+ }
97
+ /**
98
+ * Whether skill create/update/delete is enabled on this instance. Gates the
99
+ * LLM-facing `skill_*` tools (registration) and the server REST mutation
100
+ * routes. Direct programmatic `requestMutation` calls are intentionally not
101
+ * gated, so a host can still seed skills at startup.
102
+ */
103
+ get mutationsAllowed() {
104
+ return this.config.allowMutations ?? false;
105
+ }
106
+ /**
107
+ * Gate a proposed mutation through the host's onMutationRequest hook,
108
+ * then apply it when approved. No hook configured means direct apply
109
+ * (the tools themselves are already gated by allowMutations).
110
+ */
111
+ async requestMutation(action) {
112
+ let decision = { outcome: "approved" };
113
+ if (this.config.onMutationRequest) {
114
+ decision = await this.config.onMutationRequest(action);
115
+ }
116
+ if (decision.outcome !== "approved") {
117
+ return { decision };
118
+ }
119
+ // Serialize writes within this process so the name-uniqueness check and
120
+ // the store write can never interleave across concurrent mutations.
121
+ // Cross-process races remain possible with plain-object stores (S3,
122
+ // Redis) — hosts needing strict global uniqueness should serialize via
123
+ // their onMutationRequest gate; the S3 index additionally self-heals.
124
+ const run = this.mutationQueue.then(() => this.applyMutation(action));
125
+ this.mutationQueue = run.catch(() => undefined);
126
+ const skill = await run;
127
+ return { decision, ...(skill ? { skill } : {}) };
128
+ }
129
+ async applyMutation(action) {
130
+ const now = new Date().toISOString();
131
+ if (action.type === "create") {
132
+ await this.assertNameAvailable(action.skill.name);
133
+ const skill = {
134
+ id: randomUUID(),
135
+ version: 1,
136
+ status: "active",
137
+ createdAt: now,
138
+ updatedAt: now,
139
+ ...action.skill,
140
+ };
141
+ assertScopeConsistent(skill);
142
+ await this.store.put(skill);
143
+ this.invalidateIndex();
144
+ logger.info("[SkillsManager] Skill created", {
145
+ skillId: skill.id,
146
+ name: skill.name,
147
+ });
148
+ return skill;
149
+ }
150
+ if (action.type === "update") {
151
+ const existing = await this.get(action.skillId);
152
+ if (!existing) {
153
+ throw new Error(`Skill "${action.skillId}" not found`);
154
+ }
155
+ if (action.patch.name && action.patch.name !== existing.name) {
156
+ await this.assertNameAvailable(action.patch.name, existing.id);
157
+ }
158
+ const updated = {
159
+ ...existing,
160
+ ...definedFields(action.patch),
161
+ id: existing.id,
162
+ version: (existing.version ?? 1) + 1,
163
+ updatedAt: now,
164
+ };
165
+ assertScopeConsistent(updated);
166
+ await this.store.put(updated);
167
+ this.invalidateIndex();
168
+ logger.info("[SkillsManager] Skill updated", {
169
+ skillId: updated.id,
170
+ version: updated.version,
171
+ });
172
+ return updated;
173
+ }
174
+ // delete — soft: mark deprecated so the skill drops out of active
175
+ // listings but stays in storage for audit/undo.
176
+ const existing = await this.get(action.skillId);
177
+ if (!existing) {
178
+ throw new Error(`Skill "${action.skillId}" not found`);
179
+ }
180
+ if (existing.status === "deprecated") {
181
+ throw new Error(`Skill "${existing.name}" is already deleted`);
182
+ }
183
+ const deprecated = {
184
+ ...existing,
185
+ status: "deprecated",
186
+ updatedAt: now,
187
+ };
188
+ await this.store.put(deprecated);
189
+ this.invalidateIndex();
190
+ logger.info("[SkillsManager] Skill deprecated", {
191
+ skillId: existing.id,
192
+ name: existing.name,
193
+ });
194
+ return deprecated;
195
+ }
196
+ async assertNameAvailable(name, excludeId) {
197
+ const index = await this.getIndex(true);
198
+ const clash = index.find((item) => item.id !== excludeId &&
199
+ item.name.toLowerCase() === name.toLowerCase() &&
200
+ (item.status ?? "active") === "active");
201
+ if (clash) {
202
+ throw new Error(`A skill named "${name}" already exists`);
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * A scoped skill without scopeIds can never match anything — reject the
208
+ * write instead of persisting an unmatchable skill. Validated post-merge so
209
+ * both create and patch-style update paths are covered.
210
+ */
211
+ function assertScopeConsistent(skill) {
212
+ if (skill.scope === "scoped" && (skill.scopeIds ?? []).length === 0) {
213
+ throw new Error('A skill with scope "scoped" must have at least one entry in scopeIds');
214
+ }
215
+ }
216
+ /** Shallow-copy only the defined fields of a patch (undefined must not overwrite). */
217
+ function definedFields(patch) {
218
+ return Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
219
+ }
@@ -12,6 +12,7 @@ import type { NeurolinkCredentials } from "./providers.js";
12
12
  import type { ModelPoolConfig } from "./modelPool.js";
13
13
  import type { RequestRouter } from "./requestRouter.js";
14
14
  import type { ClassifierRouterConfig } from "./classifierRouter.js";
15
+ import type { SkillsConfig } from "./skills.js";
15
16
  /**
16
17
  * Main NeuroLink configuration type
17
18
  */
@@ -114,6 +115,15 @@ export type NeurolinkConstructorConfig = {
114
115
  * caller pinned both `provider` and `model`. See {@link ClassifierRouterConfig}.
115
116
  */
116
117
  classifierRouter?: ClassifierRouterConfig;
118
+ /**
119
+ * Native skills: versioned, discoverable instruction packs (SOPs,
120
+ * playbooks) with progressive disclosure. When enabled, built-in
121
+ * search_skills / list_skills tools are registered (plus gated mutation
122
+ * tools) and a compact skills index is injected into the system prompt
123
+ * of each generate()/stream() call. Opt-in and fails open on read paths.
124
+ * See {@link SkillsConfig}.
125
+ */
126
+ skills?: SkillsConfig;
117
127
  };
118
128
  /**
119
129
  * Configuration for MCP enhancement modules wired into generate()/stream() paths.
@@ -1,5 +1,6 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
2
  import type { RAGConfig } from "./rag.js";
3
+ import type { SkillsCallOptions } from "./skills.js";
3
4
  import type { AnalyticsData, TokenUsage } from "./analytics.js";
4
5
  import type { JsonValue } from "./common.js";
5
6
  import type { Content, ImageWithAltText } from "./content.js";
@@ -615,6 +616,13 @@ export type GenerateOptions = {
615
616
  * @deprecated Use `piiDetection`, `responseValidation`, and `inputValidation` instead.
616
617
  */
617
618
  processors?: ProcessorPipelineConfig;
619
+ /**
620
+ * Per-call skills control. Only effective when the instance was
621
+ * constructed with `skills.enabled: true`. Lets a call disable the
622
+ * prompt index, or narrow it by scope/tags. Per-call wins over
623
+ * instance config.
624
+ */
625
+ skills?: SkillsCallOptions;
618
626
  };
619
627
  /**
620
628
  * Represents an additional user whose memory should be included in a generate/stream call.
@@ -46,6 +46,7 @@ export * from "./scorer.js";
46
46
  export * from "./sdk.js";
47
47
  export * from "./server.js";
48
48
  export * from "./service.js";
49
+ export * from "./skills.js";
49
50
  export * from "./stream.js";
50
51
  export * from "./subscription.js";
51
52
  export * from "./task.js";
@@ -47,6 +47,7 @@ export * from "./scorer.js";
47
47
  export * from "./sdk.js";
48
48
  export * from "./server.js";
49
49
  export * from "./service.js";
50
+ export * from "./skills.js";
50
51
  export * from "./stream.js";
51
52
  export * from "./subscription.js";
52
53
  export * from "./task.js";
@@ -574,11 +574,26 @@ export type PreparedAnthropicAccountAttempt = {
574
574
  fetchStartMs?: number;
575
575
  upstreamSpan?: Span;
576
576
  };
577
+ /** How to cool an account after a genuine (non-anti-abuse) 429, derived from
578
+ * the response's quota headers + retry-after. */
579
+ export type AccountCooldownPlan = {
580
+ reason: AccountCoolingReason;
581
+ /** Epoch-ms until which the account should not be used. */
582
+ coolingUntil: number;
583
+ /** When true (5h/7d window rejected), rotate immediately — retrying the same
584
+ * account is futile until its window resets. When false (transient burst),
585
+ * a small number of jittered same-account retries is allowed first. */
586
+ rotateImmediately: boolean;
587
+ };
577
588
  export type AnthropicUpstreamFetchResult = {
578
589
  continueLoop: boolean;
579
590
  retrySameAccount?: boolean;
580
591
  /** When set, the caller should wait this many ms before retrying (from upstream retry-after). */
581
592
  retryAfterMs?: number;
593
+ /** Set on a genuine 429: how long / why to cool this account before rotating. */
594
+ cooldownPlan?: AccountCooldownPlan;
595
+ /** Quota snapshot parsed from the response headers (429 or success), if present. */
596
+ quota?: AccountQuota;
582
597
  response?: Response;
583
598
  lastError: unknown;
584
599
  sawRateLimit: boolean;
@@ -640,6 +655,12 @@ export type AccountQuota = {
640
655
  /** Epoch ms when we last captured this data */
641
656
  lastUpdated: number;
642
657
  };
658
+ /** Why an account is currently cooling. Drives cooldown duration and logging.
659
+ * - "weekly" : 7d unified limit rejected — cool until the weekly reset.
660
+ * - "session" : 5h unified limit rejected — cool until the session reset.
661
+ * - "transient" : short per-minute/burst 429 — cool for retry-after only.
662
+ * - "auth" : auth/refresh failures (reserved; currently rotate-only). */
663
+ export type AccountCoolingReason = "weekly" | "session" | "transient" | "auth";
643
664
  /** Runtime state for a proxy account. */
644
665
  export type RuntimeAccountState = {
645
666
  consecutiveRefreshFailures: number;
@@ -647,9 +668,16 @@ export type RuntimeAccountState = {
647
668
  lastToken?: string;
648
669
  lastRefreshToken?: string;
649
670
  /** Epoch-ms timestamp until which the account should not be used for new
650
- * requests (set after 429 retries are exhausted). Other requests arriving
651
- * during this window will skip the account rather than hammering it again. */
671
+ * requests. Set from the ACTUAL Anthropic reset window (5h/7d) on an
672
+ * exhaustion 429, or from retry-after on a transient burst. Other requests
673
+ * arriving during this window skip the account rather than hammering it. */
652
674
  coolingUntil?: number;
675
+ /** Why the account is cooling (set alongside coolingUntil). */
676
+ coolingReason?: AccountCoolingReason;
677
+ /** Latest quota snapshot parsed from Anthropic `anthropic-ratelimit-unified-*`
678
+ * headers on ANY response (success or 429). Drives proactive, reset-aware
679
+ * selection so we don't have to eat a 429 to discover an account is spent. */
680
+ quota?: AccountQuota;
653
681
  };
654
682
  /** A passthrough account used in the proxy route handler. */
655
683
  export type ProxyPassthroughAccount = {
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Native skills support — versioned, discoverable instruction packs
3
+ * (SOPs, playbooks, workflows) exposed to the model via progressive
4
+ * disclosure: a cheap name+description index up front, full instructions
5
+ * loaded on demand through built-in skill tools.
6
+ *
7
+ * Architecture mirrors the Hippocampus memory subsystem: a `skills` config
8
+ * on the NeuroLink constructor lazily initializes a SkillsManager, which
9
+ * auto-registers built-in tools (search_skills / list_skills, plus gated
10
+ * mutation tools) and optionally injects a compact skills index into the
11
+ * system prompt of each generate()/stream() call.
12
+ *
13
+ * Naming: every exported type carries a `Skill` prefix to satisfy the
14
+ * `unique-type-names` ESLint rule.
15
+ */
16
+ /** Visibility of a skill: available everywhere, or only in specific scopes. */
17
+ export type SkillScopeKind = "global" | "scoped";
18
+ /** Lifecycle status. Deletes are soft — deprecated skills stay in storage. */
19
+ export type SkillLifecycleStatus = "active" | "deprecated";
20
+ /**
21
+ * A complete skill: index metadata plus the full `instructions` body.
22
+ * `instructions` is the expensive part — it is only hydrated for matched
23
+ * skills, never included in index listings or the prompt index.
24
+ */
25
+ export type SkillDefinition = {
26
+ /** Stable unique identifier (UUID for created skills, or derived from filename). */
27
+ id: string;
28
+ /** Machine-friendly unique name (snake_case recommended), used for matching. */
29
+ name: string;
30
+ /** Human-readable display name shown in listings. */
31
+ displayName?: string;
32
+ /** One or two sentences describing when the skill applies — the matching signal. */
33
+ description: string;
34
+ /** Full step-by-step instructions the model follows when the skill matches. */
35
+ instructions: string;
36
+ /** Domain tags for filtering (e.g. ["payments", "escalation"]). */
37
+ tags?: string[];
38
+ /** Visibility. Default: "global". */
39
+ scope?: SkillScopeKind;
40
+ /** Scope identifiers this skill is limited to when scope === "scoped" (e.g. channel/team/tenant ids). */
41
+ scopeIds?: string[];
42
+ /** Monotonic version, bumped on every approved update. Default: 1. */
43
+ version?: number;
44
+ /** Lifecycle status. Default: "active". */
45
+ status?: SkillLifecycleStatus;
46
+ /** ISO timestamp of creation. */
47
+ createdAt?: string;
48
+ /** ISO timestamp of last update. */
49
+ updatedAt?: string;
50
+ /** Free-form host metadata (audit fields, approval references, …). */
51
+ metadata?: Record<string, unknown>;
52
+ };
53
+ /** Lightweight index entry — everything except the instructions body. */
54
+ export type SkillIndexItem = Omit<SkillDefinition, "instructions">;
55
+ /**
56
+ * Pluggable persistence backend. NeuroLink ships memory and filesystem
57
+ * stores; hosts plug their own (S3, database, …) via the "custom" storage
58
+ * type. `index()` must be cheap relative to `get()` — it backs every
59
+ * search and prompt-index build.
60
+ */
61
+ export type SkillStore = {
62
+ /** Fetch one skill (with instructions) by id. Null when absent. */
63
+ get(id: string): Promise<SkillDefinition | null>;
64
+ /** Create or replace a skill. */
65
+ put(skill: SkillDefinition): Promise<void>;
66
+ /** Hard-remove a skill from storage. (Soft deletes go through put().) */
67
+ delete(id: string): Promise<void>;
68
+ /** List index entries (no instructions) for all stored skills. */
69
+ index(): Promise<SkillIndexItem[]>;
70
+ /** Optional: drop any internal caches (called after mutations). */
71
+ invalidate?(): void;
72
+ };
73
+ /** In-process store, optionally seeded. Good for tests and embedded use. */
74
+ export type SkillMemoryStorageConfig = {
75
+ type: "memory";
76
+ /** Initial skills to seed the store with. */
77
+ skills?: SkillDefinition[];
78
+ };
79
+ /**
80
+ * Directory-backed store. Reads three layouts:
81
+ * - `<dir>/<id>.json` — one JSON-serialized SkillDefinition per file
82
+ * - `<dir>/<name>.md` — markdown with YAML frontmatter; body = instructions
83
+ * - `<dir>/<name>/SKILL.md` — Claude-skills-style directory layout
84
+ * Mutations always write `<id>.json`; markdown sources are read-only.
85
+ */
86
+ export type SkillFilesystemStorageConfig = {
87
+ type: "filesystem";
88
+ /** Directory containing skill files. Created on first write if absent. */
89
+ path: string;
90
+ };
91
+ /** Host-provided store implementation (e.g. curator's S3 repository). */
92
+ export type SkillCustomStorageConfig = {
93
+ type: "custom";
94
+ store: SkillStore;
95
+ };
96
+ /**
97
+ * S3-backed store. Layout: `<prefix>skills/<id>.json` per skill plus a
98
+ * `<prefix>index.json` document that is upserted on writes and rebuilt
99
+ * from a bucket listing when missing or corrupt (self-healing).
100
+ *
101
+ * Requires the optional peer `@aws-sdk/client-s3` to be installed in the
102
+ * host application (same pattern as memory's optional @juspay/hippocampus).
103
+ * Credentials default to the standard AWS provider chain when omitted.
104
+ */
105
+ export type SkillS3StorageConfig = {
106
+ type: "s3";
107
+ bucket: string;
108
+ /** Key prefix inside the bucket. Default: "neurolink-skills/". */
109
+ prefix?: string;
110
+ region?: string;
111
+ /** Custom endpoint (MinIO, LocalStack, …). */
112
+ endpoint?: string;
113
+ /** Use path-style addressing (required by most S3-compatible stores). */
114
+ forcePathStyle?: boolean;
115
+ credentials?: {
116
+ accessKeyId: string;
117
+ secretAccessKey: string;
118
+ sessionToken?: string;
119
+ };
120
+ };
121
+ /**
122
+ * Redis-backed store using NeuroLink's pooled Redis client (`redis` v5,
123
+ * already a core dependency). One JSON value per skill under
124
+ * `<keyPrefix><id>`; the index is derived via SCAN + MGET. Skills are
125
+ * persistent — no TTL is applied.
126
+ */
127
+ export type SkillRedisStorageConfig = {
128
+ type: "redis";
129
+ url?: string;
130
+ host?: string;
131
+ port?: number;
132
+ username?: string;
133
+ password?: string;
134
+ db?: number;
135
+ /** Key prefix. Default: "neurolink:skills:". */
136
+ keyPrefix?: string;
137
+ };
138
+ /**
139
+ * Structural surface of the lazily-required @aws-sdk/client-s3 module —
140
+ * only the pieces the S3 skill store touches (same pattern as
141
+ * HippocampusModule for the optional memory peer).
142
+ */
143
+ export type SkillS3ModuleSurface = {
144
+ S3Client: new (config: Record<string, unknown>) => {
145
+ send: (command: unknown) => Promise<unknown>;
146
+ };
147
+ GetObjectCommand: new (input: Record<string, unknown>) => unknown;
148
+ PutObjectCommand: new (input: Record<string, unknown>) => unknown;
149
+ DeleteObjectCommand: new (input: Record<string, unknown>) => unknown;
150
+ ListObjectsV2Command: new (input: Record<string, unknown>) => unknown;
151
+ };
152
+ /** Shape of the `<prefix>index.json` document maintained by the S3 store. */
153
+ export type SkillS3IndexDocument = {
154
+ lastUpdated: string;
155
+ skills: SkillIndexItem[];
156
+ };
157
+ /**
158
+ * Minimal object-storage operations the S3 skill store runs on. The
159
+ * default implementation is created lazily from @aws-sdk/client-s3;
160
+ * tests and hosts with pre-built clients can inject their own.
161
+ */
162
+ export type SkillS3ObjectOps = {
163
+ /** Fetch an object's body as a UTF-8 string. Null when the key is absent. */
164
+ getObject(key: string): Promise<string | null>;
165
+ putObject(key: string, body: string): Promise<void>;
166
+ deleteObject(key: string): Promise<void>;
167
+ /** List all object keys under a prefix (paginated internally). */
168
+ listKeys(prefix: string): Promise<string[]>;
169
+ };
170
+ export type SkillsStorageConfig = SkillMemoryStorageConfig | SkillFilesystemStorageConfig | SkillS3StorageConfig | SkillRedisStorageConfig | SkillCustomStorageConfig;
171
+ /** Query accepted by SkillsManager.search() and the search_skills tool. */
172
+ export type SkillSearchQuery = {
173
+ /** Keyword matched (case-insensitive substring) against name, displayName, and description. */
174
+ query?: string;
175
+ /** Tag filter, applied on top of the keyword match. */
176
+ tag?: string;
177
+ /** Scope filter: include global skills plus skills scoped to this id. */
178
+ scopeId?: string;
179
+ /** Maximum matches to hydrate. Defaults to SkillsConfig.maxMatches. */
180
+ limit?: number;
181
+ };
182
+ /** Input for creating a skill (id/version/status/timestamps are assigned by the manager). */
183
+ export type SkillCreateInput = {
184
+ name: string;
185
+ displayName?: string;
186
+ description: string;
187
+ instructions: string;
188
+ tags?: string[];
189
+ scope?: SkillScopeKind;
190
+ scopeIds?: string[];
191
+ metadata?: Record<string, unknown>;
192
+ };
193
+ /** Patch for updating a skill. Only provided fields change; version is bumped. */
194
+ export type SkillUpdateInput = Partial<SkillCreateInput>;
195
+ /** A proposed mutation, passed to the host's onMutationRequest gate. */
196
+ export type SkillMutationAction = {
197
+ type: "create";
198
+ skill: SkillCreateInput;
199
+ requestedBy?: string;
200
+ } | {
201
+ type: "update";
202
+ skillId: string;
203
+ patch: SkillUpdateInput;
204
+ requestedBy?: string;
205
+ } | {
206
+ type: "delete";
207
+ skillId: string;
208
+ requestedBy?: string;
209
+ };
210
+ /**
211
+ * Host decision for a proposed mutation.
212
+ * - "approved": NeuroLink applies the mutation immediately.
213
+ * - "rejected": nothing is written; `reason` is surfaced to the model.
214
+ * - "pending": the host queued the action for out-of-band approval
215
+ * (e.g. a Slack maker-checker flow) and will apply it itself later;
216
+ * `reference` is surfaced to the model (e.g. an approval ticket id).
217
+ */
218
+ export type SkillMutationDecision = {
219
+ outcome: "approved";
220
+ } | {
221
+ outcome: "rejected";
222
+ reason?: string;
223
+ } | {
224
+ outcome: "pending";
225
+ reference?: string;
226
+ };
227
+ /** Result envelope returned by SkillsManager.requestMutation(). */
228
+ export type SkillMutationResult = {
229
+ decision: SkillMutationDecision;
230
+ /** The resulting skill after an applied create/update (absent for delete/pending/rejected). */
231
+ skill?: SkillDefinition;
232
+ };
233
+ /**
234
+ * Instance-level skills configuration (NeuroLink constructor `skills` option).
235
+ * Opt-in: nothing is registered or injected unless `enabled: true`.
236
+ */
237
+ export type SkillsConfig = {
238
+ enabled: boolean;
239
+ /** Persistence backend. Default: `{ type: "memory" }`. */
240
+ storage?: SkillsStorageConfig;
241
+ /**
242
+ * Inject a compact skills index (names + descriptions, never instructions)
243
+ * into the system prompt of each generate()/stream() call. Default: true.
244
+ * Set false for curator-style pure tool-driven disclosure.
245
+ */
246
+ promptIndex?: boolean;
247
+ /** Maximum skills hydrated (with instructions) per search. Default: 5. */
248
+ maxMatches?: number;
249
+ /** Maximum entries rendered in the prompt index before truncation. Default: 50. */
250
+ promptIndexMaxItems?: number;
251
+ /** Index cache TTL in milliseconds. Default: 30000. 0 disables caching. */
252
+ indexCacheTtlMs?: number;
253
+ /** Default scope filter applied when a call/tool provides none. */
254
+ defaultScopeId?: string;
255
+ /**
256
+ * Register skill_create / skill_update / skill_delete tools so the model
257
+ * can propose skill changes. Default: false. Combine with
258
+ * `onMutationRequest` to gate writes behind host approval.
259
+ */
260
+ allowMutations?: boolean;
261
+ /**
262
+ * Host approval gate invoked before any mutation is applied. When absent
263
+ * and allowMutations is true, mutations apply directly. Errors thrown
264
+ * here reject the mutation (fail closed for writes).
265
+ */
266
+ onMutationRequest?: (action: SkillMutationAction) => Promise<SkillMutationDecision>;
267
+ };
268
+ /**
269
+ * Structural view of SkillsManager consumed by the skill tools factory —
270
+ * keeps skillTools.ts decoupled from the concrete manager class.
271
+ */
272
+ export type SkillsManagerLike = {
273
+ search: (query: SkillSearchQuery) => Promise<SkillDefinition[]>;
274
+ list: (scopeId?: string) => Promise<SkillIndexItem[]>;
275
+ requestMutation: (action: SkillMutationAction) => Promise<SkillMutationResult>;
276
+ };
277
+ /** Options for the createSkillTools factory. */
278
+ export type SkillToolsOptions = {
279
+ /** Include skill_create / skill_update / skill_delete. Default: false. */
280
+ allowMutations?: boolean;
281
+ };
282
+ /**
283
+ * Per-call skills control on generate()/stream(). Only effective when the
284
+ * instance was constructed with skills enabled; per-call wins over instance
285
+ * config (same precedence convention as per-call credentials).
286
+ */
287
+ export type SkillsCallOptions = {
288
+ /** Master toggle for this call (prompt index only — tools stay registered). Default: true. */
289
+ enabled?: boolean;
290
+ /** Per-call override of SkillsConfig.promptIndex. */
291
+ promptIndex?: boolean;
292
+ /** Scope filter for the prompt index on this call. Overrides defaultScopeId. */
293
+ scopeId?: string;
294
+ /** Restrict the prompt index to skills carrying at least one of these tags. */
295
+ tags?: string[];
296
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Native skills support — versioned, discoverable instruction packs
3
+ * (SOPs, playbooks, workflows) exposed to the model via progressive
4
+ * disclosure: a cheap name+description index up front, full instructions
5
+ * loaded on demand through built-in skill tools.
6
+ *
7
+ * Architecture mirrors the Hippocampus memory subsystem: a `skills` config
8
+ * on the NeuroLink constructor lazily initializes a SkillsManager, which
9
+ * auto-registers built-in tools (search_skills / list_skills, plus gated
10
+ * mutation tools) and optionally injects a compact skills index into the
11
+ * system prompt of each generate()/stream() call.
12
+ *
13
+ * Naming: every exported type carries a `Skill` prefix to satisfy the
14
+ * `unique-type-names` ESLint rule.
15
+ */
16
+ export {};
@@ -1,6 +1,7 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
2
  import type { EvaluationData } from "./evaluation.js";
3
3
  import type { RAGConfig } from "./rag.js";
4
+ import type { SkillsCallOptions } from "./skills.js";
4
5
  import type { AnalyticsData, ToolExecutionEvent, ToolExecutionSummary } from "../types/index.js";
5
6
  import type { MiddlewareFactoryOptions, OnChunkCallback, OnErrorCallback, OnFinishCallback } from "../types/middleware.js";
6
7
  import type { TokenUsage } from "./analytics.js";
@@ -534,6 +535,13 @@ export type StreamOptions = {
534
535
  };
535
536
  /** @deprecated Use `piiDetection`, `responseValidation`, and `inputValidation` instead. */
536
537
  processors?: ProcessorPipelineConfig;
538
+ /**
539
+ * Per-call skills control. Only effective when the instance was
540
+ * constructed with `skills.enabled: true`. Lets a call disable the
541
+ * prompt index, or narrow it by scope/tags. Per-call wins over
542
+ * instance config.
543
+ */
544
+ skills?: SkillsCallOptions;
537
545
  };
538
546
  /**
539
547
  * Stream function result type - Primary output format for streaming