@compilr-dev/sdk 0.9.16 → 0.9.18

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/dist/agent.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { Agent, ContextManager, createSuggestTool, } from '@compilr-dev/agents';
5
5
  import { createCompressorHook } from './compressors/index.js';
6
+ import { createInjectionDetectionHook } from '@compilr-dev/agents';
6
7
  import { resolveProvider } from './provider.js';
7
8
  import { resolvePreset } from './presets/index.js';
8
9
  import { assembleTools, deduplicateTools } from './tools.js';
@@ -242,13 +243,17 @@ class CompilrAgentImpl {
242
243
  : [];
243
244
  mergedHooks.beforeLLM = [...existingHooks, ...capabilityHooks];
244
245
  }
245
- // Add output compressor as the first afterTool hook (runs before delegation)
246
+ // Add output compressor and injection detection as afterTool hooks
246
247
  const existingAfterTool = config?.hooks?.afterTool
247
248
  ? Array.isArray(config.hooks.afterTool)
248
249
  ? config.hooks.afterTool
249
250
  : [config.hooks.afterTool]
250
251
  : [];
251
- mergedHooks.afterTool = [createCompressorHook(), ...existingAfterTool];
252
+ mergedHooks.afterTool = [
253
+ createCompressorHook(),
254
+ createInjectionDetectionHook(), // Scan tool results for prompt injection
255
+ ...existingAfterTool,
256
+ ];
252
257
  // Build observation mask config — SDK defaults include platform tools
253
258
  const observationMask = config?.context?.observationMask !== false
254
259
  ? {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * EntitlementCache — In-memory cache for server-driven entitlements.
3
+ *
4
+ * The server is the single source of truth. This cache:
5
+ * 1. Fetches entitlements from the server on first access
6
+ * 2. Caches in memory with a configurable TTL (default: 1 hour)
7
+ * 3. Falls back to encrypted offline backup if server is unreachable
8
+ * 4. Degrades to minimal fallback limits after offline grace period expires
9
+ *
10
+ * The host app (Desktop/CLI) provides:
11
+ * - fetchFn: how to call the server endpoint
12
+ * - store (optional): encrypted persistence for offline grace (IEntitlementStore)
13
+ */
14
+ import type { TierLimits, EntitlementResponse, LimitCheckResult } from './types.js';
15
+ /**
16
+ * Persistence adapter for offline grace period.
17
+ * Desktop: Electron safeStorage. CLI: encrypted file.
18
+ */
19
+ export interface IEntitlementStore {
20
+ /** Save encrypted entitlement data */
21
+ save(data: string): Promise<void>;
22
+ /** Load encrypted entitlement data (null if not found) */
23
+ load(): Promise<string | null>;
24
+ }
25
+ export interface EntitlementCacheConfig {
26
+ /** How to fetch entitlements from the server */
27
+ fetchFn: () => Promise<EntitlementResponse>;
28
+ /** Optional encrypted persistence for offline grace */
29
+ store?: IEntitlementStore;
30
+ /** Cache TTL in milliseconds (default: 1 hour) */
31
+ ttlMs?: number;
32
+ /** Offline grace period in milliseconds (default: 24 hours) */
33
+ offlineGraceMs?: number;
34
+ /** Called when entitlements are refreshed (for logging, UI updates) */
35
+ onRefresh?: (response: EntitlementResponse) => void;
36
+ }
37
+ export declare class EntitlementCache {
38
+ private entitlements;
39
+ private fetchedAtMonotonic;
40
+ private fetchedAtWall;
41
+ private readonly fetchFn;
42
+ private readonly store?;
43
+ private readonly ttlMs;
44
+ private readonly offlineGraceMs;
45
+ private readonly onRefresh?;
46
+ private fetchPromise;
47
+ constructor(config: EntitlementCacheConfig);
48
+ /**
49
+ * Get current tier limits. Fetches from server if stale, falls back to cache/store/defaults.
50
+ */
51
+ get(): Promise<TierLimits>;
52
+ /**
53
+ * Get the full entitlement response (tier, beta, features, etc.)
54
+ */
55
+ getResponse(): Promise<EntitlementResponse | null>;
56
+ /**
57
+ * Check if a numeric limit is exceeded.
58
+ */
59
+ checkLimit(field: keyof TierLimits, currentCount: number): LimitCheckResult;
60
+ /**
61
+ * Check if a feature flag is enabled for the current user.
62
+ */
63
+ isFeatureEnabled(featureSlug: string): boolean;
64
+ /**
65
+ * Whether the user is in beta mode (all features unlocked).
66
+ */
67
+ isBeta(): boolean;
68
+ /**
69
+ * Current tier name.
70
+ */
71
+ getTier(): string;
72
+ /**
73
+ * Force a refresh from the server (e.g., after login, after subscription change).
74
+ */
75
+ refresh(): Promise<TierLimits>;
76
+ /**
77
+ * Clear the cache (e.g., on logout).
78
+ */
79
+ clear(): void;
80
+ private isFresh;
81
+ private isWithinOfflineGrace;
82
+ private fetchAndCache;
83
+ private fallback;
84
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * EntitlementCache — In-memory cache for server-driven entitlements.
3
+ *
4
+ * The server is the single source of truth. This cache:
5
+ * 1. Fetches entitlements from the server on first access
6
+ * 2. Caches in memory with a configurable TTL (default: 1 hour)
7
+ * 3. Falls back to encrypted offline backup if server is unreachable
8
+ * 4. Degrades to minimal fallback limits after offline grace period expires
9
+ *
10
+ * The host app (Desktop/CLI) provides:
11
+ * - fetchFn: how to call the server endpoint
12
+ * - store (optional): encrypted persistence for offline grace (IEntitlementStore)
13
+ */
14
+ import { UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './types.js';
15
+ // =============================================================================
16
+ // Cache Implementation
17
+ // =============================================================================
18
+ const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
19
+ const DEFAULT_OFFLINE_GRACE_MS = 24 * 60 * 60 * 1000; // 24 hours
20
+ export class EntitlementCache {
21
+ entitlements = null;
22
+ fetchedAtMonotonic = 0n; // process.hrtime.bigint() — monotonic, not clockable
23
+ fetchedAtWall = 0; // Date.now() — for serialization only
24
+ fetchFn;
25
+ store;
26
+ ttlMs;
27
+ offlineGraceMs;
28
+ onRefresh;
29
+ fetchPromise = null; // Dedup concurrent fetches
30
+ constructor(config) {
31
+ this.fetchFn = config.fetchFn;
32
+ this.store = config.store;
33
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
34
+ this.offlineGraceMs = config.offlineGraceMs ?? DEFAULT_OFFLINE_GRACE_MS;
35
+ this.onRefresh = config.onRefresh;
36
+ }
37
+ /**
38
+ * Get current tier limits. Fetches from server if stale, falls back to cache/store/defaults.
39
+ */
40
+ async get() {
41
+ // Fresh cache → return immediately
42
+ if (this.entitlements && this.isFresh()) {
43
+ return this.entitlements.limits;
44
+ }
45
+ // Dedup concurrent fetch calls
46
+ if (this.fetchPromise)
47
+ return this.fetchPromise;
48
+ this.fetchPromise = this.fetchAndCache();
49
+ try {
50
+ return await this.fetchPromise;
51
+ }
52
+ finally {
53
+ this.fetchPromise = null;
54
+ }
55
+ }
56
+ /**
57
+ * Get the full entitlement response (tier, beta, features, etc.)
58
+ */
59
+ async getResponse() {
60
+ await this.get(); // Ensure cache is populated
61
+ return this.entitlements;
62
+ }
63
+ /**
64
+ * Check if a numeric limit is exceeded.
65
+ */
66
+ checkLimit(field, currentCount) {
67
+ if (!this.entitlements) {
68
+ return { allowed: true }; // No entitlements loaded yet — allow (fail open during init)
69
+ }
70
+ const limit = this.entitlements.limits[field];
71
+ // Unlimited
72
+ if (limit === UNLIMITED) {
73
+ return { allowed: true };
74
+ }
75
+ if (currentCount >= limit) {
76
+ return {
77
+ allowed: false,
78
+ reason: `Limit reached (${String(currentCount)}/${String(limit)})`,
79
+ upgradeHint: this.entitlements.beta
80
+ ? undefined // No upgrade prompt during beta
81
+ : 'Upgrade to Pro for unlimited access',
82
+ current: currentCount,
83
+ limit,
84
+ };
85
+ }
86
+ return { allowed: true, current: currentCount, limit };
87
+ }
88
+ /**
89
+ * Check if a feature flag is enabled for the current user.
90
+ */
91
+ isFeatureEnabled(featureSlug) {
92
+ if (!this.entitlements)
93
+ return true; // Fail open during init
94
+ return featureSlug in this.entitlements.features
95
+ ? this.entitlements.features[featureSlug]
96
+ : true; // Default: enabled if not explicitly listed
97
+ }
98
+ /**
99
+ * Whether the user is in beta mode (all features unlocked).
100
+ */
101
+ isBeta() {
102
+ return this.entitlements?.beta ?? false;
103
+ }
104
+ /**
105
+ * Current tier name.
106
+ */
107
+ getTier() {
108
+ return this.entitlements?.tier ?? 'free';
109
+ }
110
+ /**
111
+ * Force a refresh from the server (e.g., after login, after subscription change).
112
+ */
113
+ async refresh() {
114
+ this.fetchPromise = null; // Clear any pending dedup
115
+ return this.fetchAndCache();
116
+ }
117
+ /**
118
+ * Clear the cache (e.g., on logout).
119
+ */
120
+ clear() {
121
+ this.entitlements = null;
122
+ this.fetchedAtMonotonic = 0n;
123
+ this.fetchedAtWall = 0;
124
+ this.fetchPromise = null;
125
+ }
126
+ // ─── Private ───────────────────────────────────────────────────────────────
127
+ isFresh() {
128
+ if (this.fetchedAtMonotonic === 0n)
129
+ return false;
130
+ const elapsed = process.hrtime.bigint() - this.fetchedAtMonotonic;
131
+ return elapsed < BigInt(this.ttlMs) * 1000000n; // Convert ms to ns
132
+ }
133
+ isWithinOfflineGrace() {
134
+ if (this.fetchedAtMonotonic === 0n)
135
+ return false;
136
+ const elapsed = process.hrtime.bigint() - this.fetchedAtMonotonic;
137
+ return elapsed < BigInt(this.offlineGraceMs) * 1000000n;
138
+ }
139
+ async fetchAndCache() {
140
+ try {
141
+ const response = await this.fetchFn();
142
+ this.entitlements = response;
143
+ this.fetchedAtMonotonic = process.hrtime.bigint();
144
+ this.fetchedAtWall = Date.now();
145
+ // Persist encrypted for offline grace
146
+ if (this.store) {
147
+ const serialized = JSON.stringify({ response, fetchedAtWall: this.fetchedAtWall });
148
+ await this.store.save(serialized).catch(() => { }); // Best effort
149
+ }
150
+ this.onRefresh?.(response);
151
+ return response.limits;
152
+ }
153
+ catch {
154
+ // Server unreachable — try fallbacks
155
+ return this.fallback();
156
+ }
157
+ }
158
+ async fallback() {
159
+ // 1. Stale in-memory cache within offline grace
160
+ if (this.entitlements && this.isWithinOfflineGrace()) {
161
+ return this.entitlements.limits;
162
+ }
163
+ // 2. Encrypted store backup
164
+ if (this.store) {
165
+ try {
166
+ const stored = await this.store.load();
167
+ if (stored) {
168
+ const parsed = JSON.parse(stored);
169
+ const age = Date.now() - parsed.fetchedAtWall;
170
+ if (age < this.offlineGraceMs) {
171
+ this.entitlements = parsed.response;
172
+ this.fetchedAtMonotonic = process.hrtime.bigint(); // Reset monotonic to now
173
+ this.fetchedAtWall = parsed.fetchedAtWall;
174
+ return this.entitlements.limits;
175
+ }
176
+ }
177
+ }
178
+ catch {
179
+ // Corrupted store — fall through to defaults
180
+ }
181
+ }
182
+ // 3. Offline grace expired or no store — degrade to minimal fallback
183
+ return OFFLINE_FALLBACK_LIMITS;
184
+ }
185
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Entitlements — Server-driven tier management for pricing.
3
+ */
4
+ export type { TierLimits, EntitlementResponse, LimitCheckResult, } from './types.js';
5
+ export { UNLIMITED, OFFLINE_FALLBACK_LIMITS, } from './types.js';
6
+ export type { IEntitlementStore, EntitlementCacheConfig, } from './cache.js';
7
+ export { EntitlementCache } from './cache.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Entitlements — Server-driven tier management for pricing.
3
+ */
4
+ export { UNLIMITED, OFFLINE_FALLBACK_LIMITS, } from './types.js';
5
+ export { EntitlementCache } from './cache.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Entitlement Types — Shared between server, SDK, Desktop, and CLI.
3
+ *
4
+ * TierLimits values: -1 = unlimited, 0+ = actual limit.
5
+ */
6
+ /** Numeric limits for a tier. All values server-driven — never hardcoded in app logic. */
7
+ export interface TierLimits {
8
+ maxProjects: number;
9
+ maxTeamAgents: number;
10
+ maxCustomAgents: number;
11
+ maxTemplates: number;
12
+ maxBackgroundTasks: number;
13
+ maxMcpServers: number;
14
+ maxKnowledgeDocs: number;
15
+ maxConversationsPerDay: number;
16
+ maxAgentMessagesPerDay: number;
17
+ maxScaffoldToolkits: number;
18
+ }
19
+ /** Response from the cli-entitlements endpoint */
20
+ export interface EntitlementResponse {
21
+ tier: string;
22
+ beta: boolean;
23
+ limits: TierLimits;
24
+ pendingPayment: boolean;
25
+ trialEndsAt: string | null;
26
+ features: Record<string, boolean>;
27
+ issuedAt: string;
28
+ userId: string;
29
+ signature: string;
30
+ }
31
+ /** Result of checking a limit */
32
+ export interface LimitCheckResult {
33
+ allowed: boolean;
34
+ reason?: string;
35
+ upgradeHint?: string;
36
+ current?: number;
37
+ limit?: number;
38
+ }
39
+ /** Sentinel value for unlimited */
40
+ export declare const UNLIMITED = -1;
41
+ /**
42
+ * Minimal fallback limits when server is unreachable AND no cached entitlements exist.
43
+ * Intentionally restrictive to push the user to connect.
44
+ */
45
+ export declare const OFFLINE_FALLBACK_LIMITS: TierLimits;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Entitlement Types — Shared between server, SDK, Desktop, and CLI.
3
+ *
4
+ * TierLimits values: -1 = unlimited, 0+ = actual limit.
5
+ */
6
+ /** Sentinel value for unlimited */
7
+ export const UNLIMITED = -1;
8
+ /**
9
+ * Minimal fallback limits when server is unreachable AND no cached entitlements exist.
10
+ * Intentionally restrictive to push the user to connect.
11
+ */
12
+ export const OFFLINE_FALLBACK_LIMITS = {
13
+ maxProjects: 1,
14
+ maxTeamAgents: 1,
15
+ maxCustomAgents: 0,
16
+ maxTemplates: 0,
17
+ maxBackgroundTasks: 0,
18
+ maxMcpServers: 0,
19
+ maxKnowledgeDocs: 0,
20
+ maxConversationsPerDay: 5,
21
+ maxAgentMessagesPerDay: 20,
22
+ maxScaffoldToolkits: 0,
23
+ };
package/dist/index.d.ts CHANGED
@@ -58,6 +58,8 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
58
58
  export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, WorkItemCommentRecord, } from './platform/index.js';
59
59
  export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool } from './tools/index.js';
60
60
  export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, Alternative, ProposeAlternativesInput, ProposeAlternativesResult, ProposeAlternativesHandler, } from './tools/index.js';
61
+ export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './entitlements/index.js';
62
+ export type { TierLimits, EntitlementResponse, LimitCheckResult, IEntitlementStore, EntitlementCacheConfig, } from './entitlements/index.js';
61
63
  export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
62
64
  export type { DetectProjectOptions, DetectionResult, ContentSummary } from './detection/index.js';
63
65
  export { createGuideTool, SHARED_GUIDE_ENTRIES, searchGuideEntries, topicToGuideEntry, } from './guide/index.js';
package/dist/index.js CHANGED
@@ -133,6 +133,10 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
133
133
  // =============================================================================
134
134
  export { createAskUserTool, createAskUserSimpleTool, createProposeAlternativesTool } from './tools/index.js';
135
135
  // =============================================================================
136
+ // Entitlements (server-driven tier management)
137
+ // =============================================================================
138
+ export { EntitlementCache, UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './entitlements/index.js';
139
+ // =============================================================================
136
140
  // Project Detection (universal project content detection)
137
141
  // =============================================================================
138
142
  export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,7 +53,7 @@
53
53
  "node": ">=22.0.0"
54
54
  },
55
55
  "dependencies": {
56
- "@compilr-dev/agents": "^0.5.1",
56
+ "@compilr-dev/agents": "^0.5.4",
57
57
  "@compilr-dev/logger": "^0.1.0",
58
58
  "ajv": "^6.14.0"
59
59
  },