@leg3ndy/otto-bridge 1.1.6 → 1.1.8

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/README.md CHANGED
@@ -15,7 +15,7 @@ Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap
15
15
 
16
16
  Para o corte de arquitetura do `0.9.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_0_9_0_RELEASE.md).
17
17
 
18
- Para o patch atual `1.1.6`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_6_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_6_PATCH.md). Para o corte funcional da linha `1.1.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md).
18
+ Para o patch atual `1.1.8`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_8_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_8_PATCH.md). Para o corte funcional da linha `1.1.0`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md).
19
19
 
20
20
  ## Distribuicao
21
21
 
@@ -38,14 +38,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
38
38
 
39
39
  ```bash
40
40
  npm pack
41
- npm install -g ./leg3ndy-otto-bridge-1.1.6.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.1.8.tgz
42
42
  ```
43
43
 
44
- Na linha `1.1.6`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
44
+ Na linha `1.1.8`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
45
45
 
46
- No macOS, a linha `1.1.6` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
46
+ No macOS, a linha `1.1.8` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
47
47
 
48
- No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializou isso como runtime agentico; o `1.1.6` mantem a camada workspace-first com rail de coding, trust/policy por workspace, source control first-class, working set persistido e grounding remoto por repositório, enquanto endurece o `Otto Console` para manter o scrollback real sem vazar separadores do rodape e deduplicar chunks de stream sobrepostos.
48
+ No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializou isso como runtime agentico; o `1.1.8` mantem a camada workspace-first com rail de coding, trust/policy por workspace, source control first-class, working set persistido e grounding remoto por repositório, enquanto endurece o `Otto Console`, evita que `otto-bridge update` dispare setup interativo no meio da autoatualizacao e faz o scroll por wheel/trackpad funcionar no Terminal.app sem reativar click tracking.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -154,7 +154,7 @@ Dentro do console, use:
154
154
  - `/workspace clear` para limpar o binding atual do chat/sessão
155
155
  - `/new` para iniciar uma nova sessão e limpar o contexto local do console
156
156
 
157
- No TTY, o console agora reaproveita a própria tela do bridge quando você entra no `Otto Console`, em vez de abrir uma segunda viewport logo abaixo do hub. O header é impresso uma vez no topo da sessão, o histórico completo fica salvo no scrollback real do terminal e o conteúdo vai empurrando os blocos para cima naturalmente. O composer continua com placeholder `Peça algo ao Otto`, o footer mantém modelo/tokens/aprovação, e ao digitar `/` o bridge abre uma palette navegável por setas, com `Enter` preenchendo comandos como `/new` para iniciar uma nova sessão local.
157
+ No TTY, o console agora entra em `alternate screen` ao abrir o `Otto Console`, para esconder o scrollbar manual/nativo do terminal e deixar só a viewport interna do Otto. O header é impresso uma vez no topo da sessão, o transcript continua completo dentro da tela do console e o composer fica preso no rodapé com placeholder `Peça algo ao Otto`. Ao digitar `/`, o bridge abre uma palette navegável por setas, com `Enter` preenchendo comandos como `/new` para iniciar uma nova sessão local. Wheel/trackpad usam `alternate scroll` do terminal para navegar o transcript como setas; `PageUp/PageDown` continuam funcionando e cliques deixam de ser reportados como input do console.
158
158
 
159
159
  No modo `OttoAI Thinking`, o terminal agora marca explicitamente o trecho de raciocínio com `Pensando (OttoAI Thinking)` e separa esse bloco da resposta final do Otto.
160
160
 
@@ -170,7 +170,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
170
170
 
171
171
  ### WhatsApp Web em background
172
172
 
173
- Fluxo recomendado na linha `1.1.6`:
173
+ Fluxo recomendado na linha `1.1.8`:
174
174
 
175
175
  ```bash
176
176
  otto-bridge extensions --install whatsappweb
@@ -180,13 +180,13 @@ otto-bridge extensions --status whatsappweb
180
180
 
181
181
  O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
182
182
 
183
- Contrato da linha `1.1.6`:
183
+ Contrato da linha `1.1.8`:
184
184
 
185
185
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
186
186
  - `otto-bridge`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime do hub estiver ativo, sem depender de uma aba aberta no Safari
187
187
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
188
188
 
189
- ## Handoff rapido da linha 1.1.6
189
+ ## Handoff rapido da linha 1.1.8
190
190
 
191
191
  Ja fechado no codigo:
192
192
 
@@ -56,6 +56,17 @@ const DEFAULT_KEY_FILE_PATTERNS = [
56
56
  ];
57
57
  const WORKSPACE_INDEX_EXTENSION_LIMIT = 8;
58
58
  const WORKSPACE_INDEX_TOP_LEVEL_LIMIT = 16;
59
+ const WORKSPACE_INDEX_TOP_LEVEL_FILE_LIMIT = 12;
60
+ const WORKSPACE_INDEX_DIRECTORY_SAMPLE_LIMIT = 6;
61
+ const WORKSPACE_INDEX_DIRECTORY_KEY_FILE_LIMIT = 6;
62
+ const WORKSPACE_INDEX_DIRECTORY_EXTENSION_LIMIT = 4;
63
+ const WORKSPACE_INDEX_FOCUS_DIRECTORY_LIMIT = 6;
64
+ const WORKSPACE_INDEX_ENTRYPOINT_LIMIT = 12;
65
+ const WORKSPACE_INDEX_STACK_HINT_LIMIT = 8;
66
+ const WORKSPACE_INDEX_SUGGESTED_READ_LIMIT = 10;
67
+ const WORKSPACE_INDEX_SYMBOL_OUTLINE_LIMIT = 8;
68
+ const WORKSPACE_INDEX_SYMBOL_LIMIT = 8;
69
+ const WORKSPACE_INDEX_SYMBOL_FILE_SIZE_LIMIT = 64_000;
59
70
  const LOCKFILE_PACKAGE_MANAGER_MAP = {
60
71
  "package-lock.json": "npm",
61
72
  "pnpm-lock.yaml": "pnpm",
@@ -66,6 +77,67 @@ const LOCKFILE_PACKAGE_MANAGER_MAP = {
66
77
  "Cargo.lock": "cargo",
67
78
  "go.sum": "go",
68
79
  };
80
+ const WORKSPACE_INDEX_DIRECTORY_ROLE_HINTS = {
81
+ api: ["api", "backend"],
82
+ app: ["app"],
83
+ apps: ["app", "monorepo"],
84
+ backend: ["backend"],
85
+ components: ["ui"],
86
+ config: ["config"],
87
+ docs: ["docs"],
88
+ frontend: ["frontend"],
89
+ lib: ["library"],
90
+ libs: ["library"],
91
+ packages: ["monorepo"],
92
+ pages: ["routing", "frontend"],
93
+ routes: ["routing"],
94
+ scripts: ["automation"],
95
+ server: ["backend"],
96
+ services: ["services"],
97
+ src: ["source"],
98
+ test: ["tests"],
99
+ tests: ["tests"],
100
+ "__tests__": ["tests"],
101
+ web: ["frontend"],
102
+ };
103
+ const WORKSPACE_INDEX_ENTRYPOINT_FILENAMES = [
104
+ "package.json",
105
+ "pyproject.toml",
106
+ "requirements.txt",
107
+ "Cargo.toml",
108
+ "go.mod",
109
+ "README.md",
110
+ "AGENTS.md",
111
+ "tsconfig.json",
112
+ "vite.config.ts",
113
+ "vite.config.js",
114
+ "next.config.js",
115
+ "next.config.mjs",
116
+ "src/index.ts",
117
+ "src/index.tsx",
118
+ "src/main.ts",
119
+ "src/main.tsx",
120
+ "src/App.tsx",
121
+ "app/page.tsx",
122
+ "app/layout.tsx",
123
+ "server.ts",
124
+ "main.go",
125
+ "main.rs",
126
+ ];
127
+ const WORKSPACE_INDEX_CODE_EXTENSIONS = new Set([
128
+ ".ts",
129
+ ".tsx",
130
+ ".js",
131
+ ".jsx",
132
+ ".mjs",
133
+ ".cjs",
134
+ ".py",
135
+ ".go",
136
+ ".rs",
137
+ ".java",
138
+ ".kt",
139
+ ".cs",
140
+ ]);
69
141
  const WORKSPACE_OBSERVE_ONLY_ACTIONS = new Set([
70
142
  "filesystem_inspect",
71
143
  "list_files",
@@ -111,6 +183,7 @@ const WORKSPACE_POLICY_ALLOWED_ACTIONS = {
111
183
  release_operator: new Set([...WORKSPACE_POLICY_ALL_ACTIONS]),
112
184
  };
113
185
  const WORKSPACE_POLICY_CONFIRM_ACTIONS = new Set([
186
+ "run_shell",
114
187
  "git_clone",
115
188
  "git_fetch",
116
189
  "git_checkout",
@@ -151,6 +224,356 @@ function uniqueRuntimeStrings(values, limit) {
151
224
  .filter(Boolean);
152
225
  return Array.from(new Set(normalized)).slice(0, limit);
153
226
  }
227
+ function topLevelSegment(relativePath) {
228
+ const normalized = asString(relativePath).replace(/^[./]+/, "");
229
+ if (!normalized || normalized === ".") {
230
+ return "";
231
+ }
232
+ return normalized.split(/[\\/]/, 1)[0] || normalized;
233
+ }
234
+ function extensionStatsFromMap(extensionCounts, limit = WORKSPACE_INDEX_EXTENSION_LIMIT) {
235
+ return Array.from(extensionCounts.entries())
236
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
237
+ .slice(0, limit)
238
+ .map(([extension, count]) => ({ extension, count }));
239
+ }
240
+ function roleTagsForDirectory(pathValue, sampleEntries) {
241
+ const name = topLevelSegment(pathValue).toLowerCase();
242
+ const tags = new Set(WORKSPACE_INDEX_DIRECTORY_ROLE_HINTS[name] || []);
243
+ const normalizedSamples = sampleEntries.map((item) => item.toLowerCase());
244
+ if (normalizedSamples.some((item) => item.endsWith(".tsx"))) {
245
+ tags.add("react");
246
+ }
247
+ if (normalizedSamples.some((item) => item.endsWith(".ts"))) {
248
+ tags.add("typescript");
249
+ }
250
+ if (normalizedSamples.some((item) => item.endsWith(".py"))) {
251
+ tags.add("python");
252
+ }
253
+ if (normalizedSamples.some((item) => item.includes("route") || item.includes("page"))) {
254
+ tags.add("routing");
255
+ }
256
+ return Array.from(tags).sort((left, right) => left.localeCompare(right));
257
+ }
258
+ function workspaceStackHints(options) {
259
+ const hints = new Set();
260
+ const keyFiles = new Set(options.keyFiles.map((item) => item.toLowerCase()));
261
+ const extensions = new Set(options.dominantExtensions.map((item) => item.extension.toLowerCase()));
262
+ const focusNames = new Set(options.focusDirectories.map((item) => topLevelSegment(item.path).toLowerCase()));
263
+ const focusTags = new Set(options.focusDirectories.flatMap((item) => item.role_tags));
264
+ if (keyFiles.has("package.json")) {
265
+ hints.add("node");
266
+ }
267
+ if (keyFiles.has("pnpm-lock.yaml")) {
268
+ hints.add("pnpm");
269
+ }
270
+ if (keyFiles.has("package-lock.json")) {
271
+ hints.add("npm");
272
+ }
273
+ if (keyFiles.has("yarn.lock")) {
274
+ hints.add("yarn");
275
+ }
276
+ if (keyFiles.has("pyproject.toml") || keyFiles.has("requirements.txt") || keyFiles.has("poetry.lock")) {
277
+ hints.add("python");
278
+ }
279
+ if (keyFiles.has("cargo.toml")) {
280
+ hints.add("rust");
281
+ }
282
+ if (keyFiles.has("go.mod")) {
283
+ hints.add("go");
284
+ }
285
+ if (keyFiles.has("tsconfig.json") || extensions.has(".ts") || extensions.has(".tsx")) {
286
+ hints.add("typescript");
287
+ }
288
+ if (extensions.has(".js") || extensions.has(".jsx") || keyFiles.has("package.json")) {
289
+ hints.add("javascript");
290
+ }
291
+ if (extensions.has(".tsx") || focusNames.has("components") || focusTags.has("ui")) {
292
+ hints.add("react");
293
+ }
294
+ if (keyFiles.has("next.config.js") || keyFiles.has("next.config.mjs") || focusNames.has("app") || focusNames.has("pages")) {
295
+ hints.add("nextjs");
296
+ }
297
+ if (focusNames.has("server") || focusNames.has("api") || focusNames.has("backend")) {
298
+ hints.add("backend");
299
+ }
300
+ if (focusNames.has("frontend") || focusNames.has("web") || focusNames.has("components")) {
301
+ hints.add("frontend");
302
+ }
303
+ if (focusNames.has("tests") || focusNames.has("test") || focusNames.has("__tests__")) {
304
+ hints.add("tests");
305
+ }
306
+ return Array.from(hints).slice(0, WORKSPACE_INDEX_STACK_HINT_LIMIT);
307
+ }
308
+ function workspaceEntrypointPaths(options) {
309
+ const candidates = uniqueStrings([
310
+ ...WORKSPACE_INDEX_ENTRYPOINT_FILENAMES.filter((item) => options.keyFiles.includes(item) || options.topLevelFiles.includes(item)),
311
+ ...options.keyFiles,
312
+ ...options.topLevelFiles,
313
+ ...options.focusDirectories.flatMap((item) => (item.sample_entries || [])
314
+ .filter((entry) => /\.(ts|tsx|js|jsx|py|go|rs)$/i.test(entry) || /(?:^|\/)(index|main|app|server)\./i.test(entry))),
315
+ ]);
316
+ candidates.sort((left, right) => {
317
+ const leftIndex = WORKSPACE_INDEX_ENTRYPOINT_FILENAMES.indexOf(left);
318
+ const rightIndex = WORKSPACE_INDEX_ENTRYPOINT_FILENAMES.indexOf(right);
319
+ const leftScore = leftIndex >= 0 ? leftIndex : WORKSPACE_INDEX_ENTRYPOINT_FILENAMES.length + left.length;
320
+ const rightScore = rightIndex >= 0 ? rightIndex : WORKSPACE_INDEX_ENTRYPOINT_FILENAMES.length + right.length;
321
+ return leftScore - rightScore || left.localeCompare(right);
322
+ });
323
+ return candidates.slice(0, WORKSPACE_INDEX_ENTRYPOINT_LIMIT);
324
+ }
325
+ function workspaceSuggestedReadPaths(options) {
326
+ return uniqueStrings([
327
+ ...options.entrypointPaths,
328
+ ...options.keyFiles,
329
+ ...options.focusDirectories.flatMap((item) => item.sample_entries || []),
330
+ ]).slice(0, WORKSPACE_INDEX_SUGGESTED_READ_LIMIT);
331
+ }
332
+ function outlineLanguageForPath(relativePath) {
333
+ const extension = path.extname(relativePath).toLowerCase();
334
+ switch (extension) {
335
+ case ".tsx":
336
+ return "tsx";
337
+ case ".ts":
338
+ return "typescript";
339
+ case ".jsx":
340
+ return "jsx";
341
+ case ".js":
342
+ case ".mjs":
343
+ case ".cjs":
344
+ return "javascript";
345
+ case ".py":
346
+ return "python";
347
+ case ".go":
348
+ return "go";
349
+ case ".rs":
350
+ return "rust";
351
+ case ".java":
352
+ return "java";
353
+ case ".kt":
354
+ return "kotlin";
355
+ case ".cs":
356
+ return "csharp";
357
+ default:
358
+ return extension.replace(/^\./, "") || "text";
359
+ }
360
+ }
361
+ function canBuildSymbolOutline(relativePath) {
362
+ return WORKSPACE_INDEX_CODE_EXTENSIONS.has(path.extname(relativePath).toLowerCase());
363
+ }
364
+ function extractNamedList(value) {
365
+ return value
366
+ .split(",")
367
+ .map((item) => item.trim())
368
+ .map((item) => item.split(/\s+as\s+/i)[0]?.trim() || "")
369
+ .filter(Boolean);
370
+ }
371
+ function extractJavascriptLikeSymbols(content) {
372
+ const topLevel = new Set();
373
+ const exported = new Set();
374
+ const frameworkHints = new Set();
375
+ const topLevelPatterns = [
376
+ /^\s*(?:export\s+default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
377
+ /^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/gm,
378
+ /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/gm,
379
+ /^\s*export\s+(?:type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm,
380
+ ];
381
+ for (const pattern of topLevelPatterns) {
382
+ for (const match of content.matchAll(pattern)) {
383
+ const symbol = asString(match[1]);
384
+ if (symbol) {
385
+ topLevel.add(symbol);
386
+ }
387
+ }
388
+ }
389
+ const exportPatterns = [
390
+ /^\s*export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
391
+ /^\s*export\s+class\s+([A-Za-z_$][\w$]*)/gm,
392
+ /^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/gm,
393
+ /^\s*export\s+(?:type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm,
394
+ /^\s*export\s*{\s*([^}]+)\s*}/gm,
395
+ ];
396
+ for (const pattern of exportPatterns) {
397
+ for (const match of content.matchAll(pattern)) {
398
+ if (match[0]?.includes("{")) {
399
+ for (const symbol of extractNamedList(match[1] || "")) {
400
+ exported.add(symbol);
401
+ topLevel.add(symbol);
402
+ }
403
+ continue;
404
+ }
405
+ const symbol = asString(match[1]);
406
+ if (symbol) {
407
+ exported.add(symbol);
408
+ topLevel.add(symbol);
409
+ }
410
+ }
411
+ }
412
+ if (/from\s+["']react["']|React\./.test(content)) {
413
+ frameworkHints.add("react");
414
+ }
415
+ if (/from\s+["']next\/|next\/(app|server|navigation|router)/.test(content)) {
416
+ frameworkHints.add("nextjs");
417
+ }
418
+ if (/from\s+["']express["']|express\(/.test(content)) {
419
+ frameworkHints.add("express");
420
+ }
421
+ if (/from\s+["']fastify["']|fastify\(/.test(content)) {
422
+ frameworkHints.add("fastify");
423
+ }
424
+ return {
425
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
426
+ exported_symbols: Array.from(exported).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
427
+ framework_hints: Array.from(frameworkHints).slice(0, 4),
428
+ };
429
+ }
430
+ function extractPythonSymbols(content) {
431
+ const topLevel = new Set();
432
+ const frameworkHints = new Set();
433
+ for (const match of content.matchAll(/^(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(|^class\s+([A-Za-z_][\w]*)\s*[\(:]/gm)) {
434
+ const symbol = asString(match[1] || match[2]);
435
+ if (symbol) {
436
+ topLevel.add(symbol);
437
+ }
438
+ }
439
+ if (/from\s+fastapi\s+import|FastAPI\(/.test(content)) {
440
+ frameworkHints.add("fastapi");
441
+ }
442
+ if (/from\s+flask\s+import|Flask\(/.test(content)) {
443
+ frameworkHints.add("flask");
444
+ }
445
+ return {
446
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
447
+ exported_symbols: Array.from(topLevel).filter((item) => !item.startsWith("_")).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
448
+ framework_hints: Array.from(frameworkHints).slice(0, 4),
449
+ };
450
+ }
451
+ function extractGoSymbols(content) {
452
+ const topLevel = new Set();
453
+ for (const match of content.matchAll(/^(?:func|type)\s+([A-Za-z_][\w]*)/gm)) {
454
+ const symbol = asString(match[1]);
455
+ if (symbol) {
456
+ topLevel.add(symbol);
457
+ }
458
+ }
459
+ return {
460
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
461
+ exported_symbols: Array.from(topLevel).filter((item) => /^[A-Z]/.test(item)).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
462
+ framework_hints: [],
463
+ };
464
+ }
465
+ function extractRustSymbols(content) {
466
+ const topLevel = new Set();
467
+ const exported = new Set();
468
+ for (const match of content.matchAll(/^(?:pub\s+)?(?:fn|struct|enum|trait)\s+([A-Za-z_][\w]*)/gm)) {
469
+ const symbol = asString(match[1]);
470
+ if (symbol) {
471
+ topLevel.add(symbol);
472
+ if (match[0]?.startsWith("pub ")) {
473
+ exported.add(symbol);
474
+ }
475
+ }
476
+ }
477
+ return {
478
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
479
+ exported_symbols: Array.from(exported).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
480
+ framework_hints: [],
481
+ };
482
+ }
483
+ function extractJvmLikeSymbols(content) {
484
+ const topLevel = new Set();
485
+ for (const match of content.matchAll(/^(?:public\s+|private\s+|protected\s+|internal\s+)?(?:class|interface|enum|object)\s+([A-Za-z_][\w]*)/gm)) {
486
+ const symbol = asString(match[1]);
487
+ if (symbol) {
488
+ topLevel.add(symbol);
489
+ }
490
+ }
491
+ return {
492
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
493
+ exported_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
494
+ framework_hints: [],
495
+ };
496
+ }
497
+ function extractCSharpSymbols(content) {
498
+ const topLevel = new Set();
499
+ const exported = new Set();
500
+ for (const match of content.matchAll(/^(?:public\s+|internal\s+|private\s+|protected\s+)?(?:class|interface|enum|record)\s+([A-Za-z_][\w]*)/gm)) {
501
+ const symbol = asString(match[1]);
502
+ if (symbol) {
503
+ topLevel.add(symbol);
504
+ if (match[0]?.startsWith("public ") || match[0]?.startsWith("internal ")) {
505
+ exported.add(symbol);
506
+ }
507
+ }
508
+ }
509
+ return {
510
+ top_level_symbols: Array.from(topLevel).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
511
+ exported_symbols: Array.from(exported).slice(0, WORKSPACE_INDEX_SYMBOL_LIMIT),
512
+ framework_hints: [],
513
+ };
514
+ }
515
+ function buildSymbolOutline(relativePath, content) {
516
+ const extension = path.extname(relativePath).toLowerCase();
517
+ let extracted = null;
518
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(extension)) {
519
+ extracted = extractJavascriptLikeSymbols(content);
520
+ }
521
+ else if (extension === ".py") {
522
+ extracted = extractPythonSymbols(content);
523
+ }
524
+ else if (extension === ".go") {
525
+ extracted = extractGoSymbols(content);
526
+ }
527
+ else if (extension === ".rs") {
528
+ extracted = extractRustSymbols(content);
529
+ }
530
+ else if (extension === ".java" || extension === ".kt") {
531
+ extracted = extractJvmLikeSymbols(content);
532
+ }
533
+ else if (extension === ".cs") {
534
+ extracted = extractCSharpSymbols(content);
535
+ }
536
+ if (!extracted) {
537
+ return null;
538
+ }
539
+ if (extracted.top_level_symbols.length === 0 && extracted.exported_symbols.length === 0 && extracted.framework_hints.length === 0) {
540
+ return null;
541
+ }
542
+ return {
543
+ path: relativePath,
544
+ language: outlineLanguageForPath(relativePath),
545
+ top_level_symbols: extracted.top_level_symbols,
546
+ exported_symbols: extracted.exported_symbols,
547
+ framework_hints: extracted.framework_hints,
548
+ };
549
+ }
550
+ async function buildWorkspaceSymbolOutlines(basePath, candidatePaths) {
551
+ const outlines = [];
552
+ for (const relativePath of candidatePaths) {
553
+ if (outlines.length >= WORKSPACE_INDEX_SYMBOL_OUTLINE_LIMIT) {
554
+ break;
555
+ }
556
+ if (!canBuildSymbolOutline(relativePath)) {
557
+ continue;
558
+ }
559
+ const absolutePath = path.join(basePath, relativePath);
560
+ try {
561
+ const fileStats = await stat(absolutePath);
562
+ if (!fileStats.isFile() || fileStats.size > WORKSPACE_INDEX_SYMBOL_FILE_SIZE_LIMIT) {
563
+ continue;
564
+ }
565
+ const content = await readFile(absolutePath, "utf8");
566
+ const outline = buildSymbolOutline(relativePath, content);
567
+ if (outline) {
568
+ outlines.push(outline);
569
+ }
570
+ }
571
+ catch {
572
+ continue;
573
+ }
574
+ }
575
+ return outlines;
576
+ }
154
577
  function normalizeWorkspaceMemory(value, workspaceId) {
155
578
  if (!value) {
156
579
  return undefined;
@@ -661,8 +1084,10 @@ export async function buildWorkspaceIndex(options) {
661
1084
  ]));
662
1085
  const queue = [{ directoryPath: basePath, depth: 0 }];
663
1086
  const topLevelEntries = [];
1087
+ const topLevelFiles = [];
664
1088
  const keyFiles = new Set();
665
1089
  const extensionCounts = new Map();
1090
+ const directorySummaryByPath = new Map();
666
1091
  let scannedDirectoryCount = 0;
667
1092
  let scannedFileCount = 0;
668
1093
  let truncated = false;
@@ -684,6 +1109,10 @@ export async function buildWorkspaceIndex(options) {
684
1109
  continue;
685
1110
  }
686
1111
  entries.sort((left, right) => left.name.localeCompare(right.name));
1112
+ const currentRelativePath = toRelativePath(basePath, next.directoryPath);
1113
+ const currentDirectorySummary = next.depth === 1
1114
+ ? directorySummaryByPath.get(currentRelativePath)
1115
+ : undefined;
687
1116
  for (const entry of entries) {
688
1117
  const candidatePath = path.join(next.directoryPath, entry.name);
689
1118
  const relativePath = toRelativePath(basePath, candidatePath);
@@ -697,6 +1126,24 @@ export async function buildWorkspaceIndex(options) {
697
1126
  if (ignoredDirectoryNames.has(entry.name)) {
698
1127
  continue;
699
1128
  }
1129
+ if (next.depth === 0) {
1130
+ directorySummaryByPath.set(relativePath, {
1131
+ path: relativePath,
1132
+ child_file_count: 0,
1133
+ child_directory_count: 0,
1134
+ key_files: [],
1135
+ dominant_extensions: [],
1136
+ sample_entries: [],
1137
+ role_tags: [],
1138
+ extensionCounts: new Map(),
1139
+ });
1140
+ }
1141
+ if (currentDirectorySummary) {
1142
+ currentDirectorySummary.child_directory_count += 1;
1143
+ if (currentDirectorySummary.sample_entries.length < WORKSPACE_INDEX_DIRECTORY_SAMPLE_LIMIT) {
1144
+ currentDirectorySummary.sample_entries.push(relativePath);
1145
+ }
1146
+ }
700
1147
  if (next.depth + 1 <= maxDepth) {
701
1148
  queue.push({
702
1149
  directoryPath: candidatePath,
@@ -712,21 +1159,79 @@ export async function buildWorkspaceIndex(options) {
712
1159
  continue;
713
1160
  }
714
1161
  scannedFileCount += 1;
1162
+ if (next.depth === 0 && topLevelFiles.length < WORKSPACE_INDEX_TOP_LEVEL_FILE_LIMIT) {
1163
+ topLevelFiles.push(relativePath);
1164
+ }
715
1165
  if (keyFilePatterns.has(entry.name)) {
716
1166
  keyFiles.add(relativePath);
1167
+ if (currentDirectorySummary && currentDirectorySummary.key_files.length < WORKSPACE_INDEX_DIRECTORY_KEY_FILE_LIMIT) {
1168
+ currentDirectorySummary.key_files.push(relativePath);
1169
+ }
717
1170
  }
718
1171
  const extension = path.extname(entry.name).toLowerCase() || "[no_ext]";
719
1172
  extensionCounts.set(extension, (extensionCounts.get(extension) || 0) + 1);
1173
+ if (currentDirectorySummary) {
1174
+ currentDirectorySummary.child_file_count += 1;
1175
+ currentDirectorySummary.extensionCounts.set(extension, (currentDirectorySummary.extensionCounts.get(extension) || 0) + 1);
1176
+ if (currentDirectorySummary.sample_entries.length < WORKSPACE_INDEX_DIRECTORY_SAMPLE_LIMIT) {
1177
+ currentDirectorySummary.sample_entries.push(relativePath);
1178
+ }
1179
+ }
720
1180
  if (scannedFileCount >= maxFiles) {
721
1181
  truncated = true;
722
1182
  break;
723
1183
  }
724
1184
  }
725
1185
  }
726
- const dominantExtensions = Array.from(extensionCounts.entries())
727
- .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
728
- .slice(0, WORKSPACE_INDEX_EXTENSION_LIMIT)
729
- .map(([extension, count]) => ({ extension, count }));
1186
+ const dominantExtensions = extensionStatsFromMap(extensionCounts, WORKSPACE_INDEX_EXTENSION_LIMIT);
1187
+ const directorySummaries = Array.from(directorySummaryByPath.values())
1188
+ .map((item) => {
1189
+ const sampleEntries = uniqueStrings(item.sample_entries).slice(0, WORKSPACE_INDEX_DIRECTORY_SAMPLE_LIMIT);
1190
+ return {
1191
+ path: item.path,
1192
+ child_file_count: item.child_file_count,
1193
+ child_directory_count: item.child_directory_count,
1194
+ key_files: uniqueStrings(item.key_files).slice(0, WORKSPACE_INDEX_DIRECTORY_KEY_FILE_LIMIT),
1195
+ dominant_extensions: extensionStatsFromMap(item.extensionCounts, WORKSPACE_INDEX_DIRECTORY_EXTENSION_LIMIT),
1196
+ sample_entries: sampleEntries,
1197
+ role_tags: roleTagsForDirectory(item.path, sampleEntries),
1198
+ };
1199
+ })
1200
+ .sort((left, right) => left.path.localeCompare(right.path));
1201
+ const focusDirectories = [...directorySummaries]
1202
+ .sort((left, right) => {
1203
+ const leftScore = (left.role_tags.length * 10) + left.child_file_count + left.child_directory_count + (left.key_files.length * 2);
1204
+ const rightScore = (right.role_tags.length * 10) + right.child_file_count + right.child_directory_count + (right.key_files.length * 2);
1205
+ return rightScore - leftScore || left.path.localeCompare(right.path);
1206
+ })
1207
+ .filter((item) => item.child_file_count > 0 || item.child_directory_count > 0 || item.role_tags.length > 0)
1208
+ .slice(0, WORKSPACE_INDEX_FOCUS_DIRECTORY_LIMIT);
1209
+ const normalizedKeyFiles = Array.from(keyFiles).sort((left, right) => left.localeCompare(right));
1210
+ const stackHints = workspaceStackHints({
1211
+ keyFiles: normalizedKeyFiles,
1212
+ dominantExtensions,
1213
+ focusDirectories,
1214
+ });
1215
+ const entrypointPaths = workspaceEntrypointPaths({
1216
+ keyFiles: normalizedKeyFiles,
1217
+ topLevelFiles,
1218
+ focusDirectories,
1219
+ });
1220
+ const suggestedReadPaths = workspaceSuggestedReadPaths({
1221
+ keyFiles: normalizedKeyFiles,
1222
+ entrypointPaths,
1223
+ focusDirectories,
1224
+ });
1225
+ const symbolOutlines = await buildWorkspaceSymbolOutlines(basePath, suggestedReadPaths);
1226
+ const focusSummary = focusDirectories.length > 0
1227
+ ? ` Areas foco: ${focusDirectories.map((item) => item.path).join(", ")}.`
1228
+ : "";
1229
+ const stackSummary = stackHints.length > 0
1230
+ ? ` Stack sugerida: ${stackHints.join(", ")}.`
1231
+ : "";
1232
+ const outlineSummary = symbolOutlines.length > 0
1233
+ ? ` Outline inicial: ${symbolOutlines.map((item) => item.path).join(", ")}.`
1234
+ : "";
730
1235
  return {
731
1236
  resolver,
732
1237
  workspace_id: options.workspaceId,
@@ -735,9 +1240,16 @@ export async function buildWorkspaceIndex(options) {
735
1240
  scanned_file_count: scannedFileCount,
736
1241
  truncated,
737
1242
  top_level_entries: topLevelEntries,
738
- key_files: Array.from(keyFiles).sort((left, right) => left.localeCompare(right)),
1243
+ top_level_files: topLevelFiles,
1244
+ key_files: normalizedKeyFiles,
739
1245
  dominant_extensions: dominantExtensions,
740
- summary: `Indexei ${scannedFileCount} arquivo${scannedFileCount === 1 ? "" : "s"} e ${scannedDirectoryCount} diretorio${scannedDirectoryCount === 1 ? "" : "s"} em ${path.basename(basePath) || basePath}.`,
1246
+ directory_summaries: directorySummaries,
1247
+ focus_directories: focusDirectories,
1248
+ stack_hints: stackHints,
1249
+ entrypoint_paths: entrypointPaths,
1250
+ suggested_read_paths: suggestedReadPaths,
1251
+ symbol_outlines: symbolOutlines,
1252
+ summary: `Indexei ${scannedFileCount} arquivo${scannedFileCount === 1 ? "" : "s"} e ${scannedDirectoryCount} diretorio${scannedDirectoryCount === 1 ? "" : "s"} em ${path.basename(basePath) || basePath}.${stackSummary}${focusSummary}${outlineSummary}`.trim(),
741
1253
  };
742
1254
  }
743
1255
  export async function buildRepoManifest(options) {