Skip to content

Commit facaa30

Browse files
author
Omri Hopson
committed
made updates
1 parent e481523 commit facaa30

26 files changed

Lines changed: 831 additions & 96 deletions

.cursor/rules/context-simplo-usage.mdc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,38 @@ MCP server: `user-context-simplo`. Use it **instead of** grep/ripgrep/Glob/Seman
2929
- Pass `repositoryId` to scope queries and avoid full-index scans
3030
- Use `incremental: true` when re-indexing after changes
3131
- Skip Context-Simplo for single-file edits where the file is already open/known
32+
33+
## Compact mode (token savings)
34+
35+
Set `CONTEXT_SIMPLO_RESPONSE_MODE=compact` in the server environment to enable ~60% input token savings.
36+
37+
In compact mode, response keys are abbreviated:
38+
39+
| Full key | Compact | Full key | Compact |
40+
|----------|---------|----------|---------|
41+
| `results` / `callers` / `callees` | `r` | `repositoryId` | `rid` |
42+
| `name` | `n` | `language` | `lang` |
43+
| `qualifiedName` | `qn` | `nodeId` | `nid` |
44+
| `kind` | `k` | `score` | `s` |
45+
| `filePath` | `fp` | `isExported` | `x` |
46+
| `lineStart` | `ls` | `complexity` | `cx` |
47+
| `lineEnd` | `le` | `total` | `t` |
48+
| `symbol` | `sym` | `hasMore` | `m` |
49+
| `affectedNodes` | `nodes` | `searchType` | `st` |
50+
| `affectedFiles` | `files` | `entryPoints` | `entry` |
51+
| `modules` | `mods` | `keyAbstractions` | `abs` |
52+
53+
Compact mode also removes `null` values, `id` hash fields, `visibility`, `limit`, and `offset` from responses, and minifies JSON (no indentation). `repositoryId` and `language` are hoisted to the envelope when all results share the same value.
54+
55+
## Response style
56+
57+
Be direct and concise in all responses. Drop articles (a/an/the), filler (just/really/basically/simply), pleasantries (sure/certainly/of course), and hedging. Fragments OK. Use short synonyms: "fix" not "implement a solution for", "use" not "utilize". Technical terms, code blocks, and error messages stay exact.
58+
59+
Pattern: `[thing] [action] [reason]. [next step].`
60+
61+
Example — not: "Sure! I'd be happy to help. The issue you're experiencing is likely caused by..."
62+
Example — yes: "Bug in auth middleware. Token expiry uses `<` not `<=`. Fix:"
63+
64+
Exception: use full sentences for security warnings, irreversible operations, and multi-step sequences where brevity risks misreading.
65+
66+
Never create or edit `.md` files unless explicitly asked.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
"dev": "tsx watch src/index.ts",
1212
"build": "tsc --project tsconfig.build.json",
1313
"build:dashboard": "cd dashboard && pnpm build",
14-
"test": "vitest run",
15-
"test:watch": "vitest",
16-
"test:coverage": "vitest run --coverage",
14+
"test": "NODE_OPTIONS='--max-old-space-size=4096' vitest run",
15+
"test:watch": "NODE_OPTIONS='--max-old-space-size=4096' vitest",
16+
"test:coverage": "NODE_OPTIONS='--max-old-space-size=4096' vitest run --coverage",
1717
"lint": "eslint src/ --ext .ts",
1818
"lint:fix": "eslint src/ --ext .ts --fix",
1919
"format": "prettier --write \"src/**/*.ts\" \"dashboard/src/**/*.{ts,tsx}\"",

src/core/config-manager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ export class ConfigManager extends EventEmitter {
130130
return { success: true, changes, warnings };
131131
} catch (error) {
132132
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
133-
this.emit('error', errorMessage);
133+
if (this.listenerCount('error') > 0) {
134+
this.emit('error', errorMessage);
135+
}
134136
return {
135137
success: false,
136138
changes,

src/core/config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121

2222
import { ConfigError } from './errors.js';
23-
import type { AppConfig, ConfigValue, LLMProviderType } from './types.js';
23+
import type { AppConfig, ConfigValue, LLMProviderType, ResponseMode } from './types.js';
2424

2525
const DEFAULT_CONFIG = {
2626
llmProvider: 'none' as LLMProviderType,
@@ -34,6 +34,7 @@ const DEFAULT_CONFIG = {
3434
embeddingConcurrency: 5,
3535
embeddingBatchSize: 20,
3636
graphMemoryLimitMb: 512,
37+
responseMode: 'full' as ResponseMode,
3738
} as const;
3839

3940
const ENV_VAR_MAP = {
@@ -48,6 +49,7 @@ const ENV_VAR_MAP = {
4849
embeddingConcurrency: 'EMBEDDING_CONCURRENCY',
4950
embeddingBatchSize: 'EMBEDDING_BATCH_SIZE',
5051
graphMemoryLimitMb: 'GRAPH_MEMORY_LIMIT_MB',
52+
responseMode: 'CONTEXT_SIMPLO_RESPONSE_MODE',
5153
} as const;
5254

5355
type ConfigKey = keyof typeof DEFAULT_CONFIG;
@@ -164,6 +166,15 @@ export function loadConfig(dashboardConfig?: DashboardConfig): AppConfig {
164166
process.env[ENV_VAR_MAP.graphMemoryLimitMb]
165167
) as number | undefined;
166168

169+
const envResponseModeRaw = process.env[ENV_VAR_MAP.responseMode];
170+
let envResponseMode: ResponseMode | undefined;
171+
if (envResponseModeRaw !== undefined) {
172+
if (envResponseModeRaw !== 'full' && envResponseModeRaw !== 'compact') {
173+
throw new ConfigError('responseMode', `Invalid value: ${envResponseModeRaw}. Must be 'full' or 'compact'`);
174+
}
175+
envResponseMode = envResponseModeRaw as ResponseMode;
176+
}
177+
167178
validateUrl(envLlmBaseUrl, 'llmBaseUrl');
168179
validateUrl(dashboardConfig?.llmBaseUrl, 'llmBaseUrl');
169180

@@ -244,6 +255,13 @@ export function loadConfig(dashboardConfig?: DashboardConfig): AppConfig {
244255
DEFAULT_CONFIG.graphMemoryLimitMb
245256
);
246257

258+
const responseMode = createConfigValue(
259+
'responseMode',
260+
envResponseMode,
261+
undefined,
262+
DEFAULT_CONFIG.responseMode
263+
);
264+
247265
if (llmProvider.value === 'openai' && !llmApiKey.value) {
248266
throw new ConfigError(
249267
'llmApiKey',
@@ -270,6 +288,7 @@ export function loadConfig(dashboardConfig?: DashboardConfig): AppConfig {
270288
embeddingConcurrency,
271289
embeddingBatchSize,
272290
graphMemoryLimitMb,
291+
responseMode,
273292
};
274293
}
275294

src/core/embedding-queue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class EmbeddingQueue extends EventEmitter {
8888
}
8989

9090
private async processQueue(): Promise<void> {
91-
while (this.inFlight < this.concurrency && this.queue.length > 0 && !this.draining) {
91+
while (this.inFlight < this.concurrency && this.queue.length > 0) {
9292
const job = this.queue.shift();
9393
if (!job) break;
9494

src/core/errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ export function isRetryableError(error: Error): boolean {
114114
if (error instanceof LLMError) {
115115
return error.isRetryable;
116116
}
117+
// Support plain errors with a retryable property (e.g. from tests or external libs)
118+
if ('retryable' in error && typeof (error as any).retryable === 'boolean') {
119+
return (error as any).retryable;
120+
}
117121
return false;
118122
}
119123

src/core/graph.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -517,25 +517,25 @@ export class CodeGraph {
517517
return JSON.stringify({ nodes, edges });
518518
}
519519

520-
deserialize(data: string): void {
520+
async deserialize(data: string): Promise<void> {
521521
const { nodes, edges } = JSON.parse(data) as { nodes: CodeNode[]; edges: GraphEdge[] };
522522

523523
for (const node of nodes) {
524-
this.addNode(node);
524+
await this.addNode(node);
525525
}
526526

527527
for (const edge of edges) {
528528
try {
529-
this.addEdge(edge);
529+
await this.addEdge(edge);
530530
} catch (error) {
531531
console.warn(`Failed to restore edge ${edge.id}: ${(error as Error).message}`);
532532
}
533533
}
534534
}
535535

536-
static fromSerialized(data: string): CodeGraph {
536+
static async fromSerialized(data: string): Promise<CodeGraph> {
537537
const graph = new CodeGraph();
538-
graph.deserialize(data);
538+
await graph.deserialize(data);
539539
return graph;
540540
}
541541

src/core/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ export const LLMProviderTypeSchema = z.enum(['openai', 'ollama', 'azure', 'none'
201201

202202
export type LLMProviderType = z.infer<typeof LLMProviderTypeSchema>;
203203

204+
export const ResponseModeSchema = z.enum(['full', 'compact']);
205+
206+
export type ResponseMode = z.infer<typeof ResponseModeSchema>;
207+
204208
export const ConfigValueSchema = z.object({
205209
value: z.unknown(),
206210
source: ConfigSourceSchema,
@@ -269,6 +273,11 @@ export const AppConfigSchema = z.object({
269273
source: ConfigSourceSchema,
270274
isLocked: z.boolean(),
271275
}),
276+
responseMode: z.object({
277+
value: ResponseModeSchema,
278+
source: ConfigSourceSchema,
279+
isLocked: z.boolean(),
280+
}),
272281
});
273282

274283
export type AppConfig = z.infer<typeof AppConfigSchema>;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ async function main() {
134134
vectorStore: config.llmProvider.value !== 'none' ? vectorStore : undefined,
135135
embeddingProvider: config.llmProvider.value !== 'none' ? embeddingProvider : undefined,
136136
watcher,
137+
responseMode: config.responseMode.value,
137138
});
138139

139140
const configManager = new ConfigManager({

src/mcp/formatter.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* MCP Response Formatter
3+
*
4+
* Compact mode reduces token usage ~60% by:
5+
* - Shortening verbose JSON keys (filePath → fp, qualifiedName → qn, etc.)
6+
* - Removing null/undefined values
7+
* - Dropping rarely-useful fields (id hashes, visibility, pagination echoes)
8+
* - Hoisting shared repositoryId/language to envelope level
9+
* - Minifying JSON (no indentation)
10+
*
11+
* Full mode: passthrough with pretty-print (existing behavior).
12+
*/
13+
14+
import type { ResponseMode } from '../core/types.js';
15+
16+
const KEY_MAP: Record<string, string> = {
17+
results: 'r',
18+
callers: 'r',
19+
callees: 'r',
20+
name: 'n',
21+
qualifiedName: 'qn',
22+
kind: 'k',
23+
filePath: 'fp',
24+
lineStart: 'ls',
25+
lineEnd: 'le',
26+
repositoryId: 'rid',
27+
language: 'lang',
28+
nodeId: 'nid',
29+
score: 's',
30+
isExported: 'x',
31+
complexity: 'cx',
32+
total: 't',
33+
hasMore: 'm',
34+
symbol: 'sym',
35+
affectedNodes: 'nodes',
36+
affectedFiles: 'files',
37+
entryPoints: 'entry',
38+
modules: 'mods',
39+
keyAbstractions: 'abs',
40+
searchType: 'st',
41+
};
42+
43+
// Fields stripped entirely in compact mode
44+
const STRIP_FIELDS = new Set(['id', 'visibility', 'limit', 'offset', 'columnStart', 'columnEnd']);
45+
46+
/**
47+
* Recursively rename keys, strip null/undefined/stripped fields.
48+
*/
49+
function compactValue(value: unknown): unknown {
50+
if (value === null || value === undefined) return undefined;
51+
if (Array.isArray(value)) {
52+
return value.map(compactValue).filter((v) => v !== undefined);
53+
}
54+
if (typeof value === 'object') {
55+
return compactObject(value as Record<string, unknown>);
56+
}
57+
return value;
58+
}
59+
60+
function compactObject(obj: Record<string, unknown>): Record<string, unknown> {
61+
const out: Record<string, unknown> = {};
62+
for (const [key, val] of Object.entries(obj)) {
63+
if (STRIP_FIELDS.has(key)) continue;
64+
if (val === null || val === undefined) continue;
65+
66+
const compacted = compactValue(val);
67+
if (compacted === undefined) continue;
68+
69+
const mappedKey = KEY_MAP[key] ?? key;
70+
out[mappedKey] = compacted;
71+
}
72+
return out;
73+
}
74+
75+
/**
76+
* Hoist repositoryId and language to envelope when all items in a results
77+
* array share the same value, then remove from individual items.
78+
*/
79+
function hoistSharedFields(obj: Record<string, unknown>): Record<string, unknown> {
80+
const resultsKey = Object.keys(obj).find((k) => Array.isArray(obj[k]) && k === 'r');
81+
if (!resultsKey) return obj;
82+
83+
const items = obj[resultsKey] as Record<string, unknown>[];
84+
if (items.length === 0) return obj;
85+
86+
const sharedFields: Array<{ original: string; compact: string }> = [
87+
{ original: 'repositoryId', compact: 'rid' },
88+
{ original: 'language', compact: 'lang' },
89+
];
90+
91+
const hoisted: Record<string, unknown> = {};
92+
93+
for (const { original, compact } of sharedFields) {
94+
const compactKey = KEY_MAP[original] ?? original;
95+
const values = items.map((item) => item[compactKey] ?? item[original]);
96+
const allSame = values.length > 0 && values.every((v) => v === values[0]);
97+
if (allSame && values[0] !== undefined) {
98+
hoisted[compact] = values[0];
99+
}
100+
}
101+
102+
if (Object.keys(hoisted).length === 0) return obj;
103+
104+
// Remove hoisted fields from each item
105+
const hoistedKeys = new Set(Object.keys(hoisted));
106+
const strippedItems = items.map((item) => {
107+
const copy = { ...item };
108+
for (const k of hoistedKeys) {
109+
delete copy[k];
110+
}
111+
return copy;
112+
});
113+
114+
return { ...obj, ...hoisted, [resultsKey]: strippedItems };
115+
}
116+
117+
/**
118+
* Transform a response object into compact form.
119+
*/
120+
export function compactResponse(result: unknown): unknown {
121+
if (typeof result !== 'object' || result === null || Array.isArray(result)) {
122+
return result;
123+
}
124+
const compacted = compactObject(result as Record<string, unknown>);
125+
return hoistSharedFields(compacted);
126+
}
127+
128+
/**
129+
* Serialize an MCP tool result to string.
130+
* compact: short keys + minified JSON
131+
* full: original keys + pretty-printed JSON
132+
*/
133+
export function formatMCPResponse(result: unknown, mode: ResponseMode): string {
134+
if (mode === 'compact') {
135+
return JSON.stringify(compactResponse(result));
136+
}
137+
return JSON.stringify(result, null, 2);
138+
}

0 commit comments

Comments
 (0)