@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,645 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import {
5
+ RepositoryStructureArtifactSchema,
6
+ RepositoryStructureSummarySchema,
7
+ } from '@lota-sdk/shared/schemas/repository-structure'
8
+ import type {
9
+ RepositoryStructureArtifact,
10
+ RepositoryStructureComponent,
11
+ RepositoryStructureShape,
12
+ RepositoryStructureSignal,
13
+ RepositoryStructureSummary,
14
+ } from '@lota-sdk/shared/schemas/repository-structure'
15
+
16
+ const EXTRACTOR_VERSION = 'repository-structure-extractor.v1'
17
+ const IGNORED_DIR_NAMES = new Set([
18
+ '.git',
19
+ '.idea',
20
+ '.next',
21
+ '.turbo',
22
+ '.vscode',
23
+ 'build',
24
+ 'coverage',
25
+ 'dist',
26
+ 'node_modules',
27
+ 'target',
28
+ ])
29
+
30
+ const LANGUAGE_BY_EXTENSION: Record<string, string> = {
31
+ '.cjs': 'JavaScript',
32
+ '.cpp': 'C++',
33
+ '.cs': 'C#',
34
+ '.go': 'Go',
35
+ '.java': 'Java',
36
+ '.js': 'JavaScript',
37
+ '.jsx': 'React JSX',
38
+ '.kt': 'Kotlin',
39
+ '.md': 'Markdown',
40
+ '.mjs': 'JavaScript',
41
+ '.php': 'PHP',
42
+ '.py': 'Python',
43
+ '.rb': 'Ruby',
44
+ '.rs': 'Rust',
45
+ '.sql': 'SQL',
46
+ '.swift': 'Swift',
47
+ '.ts': 'TypeScript',
48
+ '.tsx': 'React TSX',
49
+ '.vue': 'Vue',
50
+ }
51
+
52
+ type PackageJson = {
53
+ name?: string
54
+ private?: boolean
55
+ scripts?: Record<string, string>
56
+ dependencies?: Record<string, string>
57
+ devDependencies?: Record<string, string>
58
+ }
59
+
60
+ type ExtractParams = {
61
+ rootDir: string
62
+ repositoryKey: string
63
+ repositoryFullName: string
64
+ defaultBranch: string
65
+ indexBranch: string
66
+ commitSha: string | undefined
67
+ }
68
+
69
+ type ExtractResult = {
70
+ repoStructure: RepositoryStructureArtifact
71
+ structureSummary: RepositoryStructureSummary
72
+ structureMarkdown: string
73
+ }
74
+
75
+ function normalizePath(value: string): string {
76
+ return value.replaceAll(path.sep, '/')
77
+ }
78
+
79
+ function unique<T>(values: T[]): T[] {
80
+ return [...new Set(values)]
81
+ }
82
+
83
+ function truncate<T>(values: T[], limit: number): T[] {
84
+ return values.slice(0, limit)
85
+ }
86
+
87
+ function normalizeEvidencePaths(values: string[]): string[] {
88
+ return truncate(unique(values), 20)
89
+ }
90
+
91
+ function createSignal(params: {
92
+ key: string
93
+ label: string
94
+ reason: string
95
+ evidencePaths?: string[]
96
+ }): RepositoryStructureSignal {
97
+ return {
98
+ key: params.key,
99
+ label: params.label,
100
+ reason: params.reason,
101
+ evidencePaths: normalizeEvidencePaths(params.evidencePaths ?? []),
102
+ }
103
+ }
104
+
105
+ async function collectRelativeFilePaths(rootDir: string, currentDir = ''): Promise<string[]> {
106
+ const absoluteDir = path.join(rootDir, currentDir)
107
+ const entries = await readdir(absoluteDir, { withFileTypes: true })
108
+ const filePaths: string[] = []
109
+
110
+ for (const entry of entries) {
111
+ const relativePath = normalizePath(path.join(currentDir, entry.name))
112
+ if (entry.isDirectory()) {
113
+ if (IGNORED_DIR_NAMES.has(entry.name)) continue
114
+ filePaths.push(...(await collectRelativeFilePaths(rootDir, relativePath)))
115
+ continue
116
+ }
117
+ filePaths.push(relativePath)
118
+ }
119
+
120
+ return filePaths
121
+ }
122
+
123
+ async function readPackageJson(rootDir: string, relativePath: string): Promise<PackageJson | null> {
124
+ try {
125
+ const raw = await readFile(path.join(rootDir, relativePath), 'utf8')
126
+ return JSON.parse(raw) as PackageJson
127
+ } catch {
128
+ return null
129
+ }
130
+ }
131
+
132
+ function gatherTopLevelEntries(filePaths: string[]): string[] {
133
+ return truncate(
134
+ [...new Set(filePaths.map((filePath) => filePath.split('/')[0]).filter((entry) => entry && entry !== ''))].sort(),
135
+ 200,
136
+ )
137
+ }
138
+
139
+ function gatherLanguageSignals(filePaths: string[]): RepositoryStructureSignal[] {
140
+ const languageEvidence = new Map<string, string[]>()
141
+
142
+ for (const filePath of filePaths) {
143
+ const language = LANGUAGE_BY_EXTENSION[path.extname(filePath).toLowerCase()]
144
+ if (!language) continue
145
+ const entries = languageEvidence.get(language) ?? []
146
+ entries.push(filePath)
147
+ languageEvidence.set(language, entries)
148
+ }
149
+
150
+ return [...languageEvidence.entries()].map(([language, evidencePaths]) =>
151
+ createSignal({
152
+ key: language.toLowerCase().replaceAll(/\s+/g, '-'),
153
+ label: language,
154
+ reason: `Detected ${language} source files in the repository.`,
155
+ evidencePaths,
156
+ }),
157
+ )
158
+ }
159
+
160
+ function findPaths(filePaths: string[], matcher: (filePath: string) => boolean): string[] {
161
+ return filePaths.filter(matcher)
162
+ }
163
+
164
+ function buildPathSignals(params: {
165
+ keyPrefix: string
166
+ label: string
167
+ reason: string
168
+ filePaths: string[]
169
+ }): RepositoryStructureSignal[] {
170
+ return params.filePaths.map((filePath, index) =>
171
+ createSignal({
172
+ key: `${params.keyPrefix}-${index + 1}`,
173
+ label: filePath,
174
+ reason: params.reason,
175
+ evidencePaths: [filePath],
176
+ }),
177
+ )
178
+ }
179
+
180
+ function inferFrameworkSignals(packageJsons: Array<{ path: string; data: PackageJson }>): RepositoryStructureSignal[] {
181
+ const dependencyMap = new Map<string, { label: string; reason: string; evidencePaths: string[] }>()
182
+
183
+ const register = (dependencyName: string, label: string, reason: string, evidencePath: string) => {
184
+ const current = dependencyMap.get(dependencyName) ?? { label, reason, evidencePaths: [] }
185
+ current.evidencePaths.push(evidencePath)
186
+ dependencyMap.set(dependencyName, current)
187
+ }
188
+
189
+ for (const packageJson of packageJsons) {
190
+ const dependencyNames = [
191
+ ...Object.keys(packageJson.data.dependencies ?? {}),
192
+ ...Object.keys(packageJson.data.devDependencies ?? {}),
193
+ ]
194
+ for (const dependencyName of dependencyNames) {
195
+ if (dependencyName === 'next') {
196
+ register('next', 'Next.js', 'Detected the Next.js framework dependency.', packageJson.path)
197
+ }
198
+ if (dependencyName === 'react') {
199
+ register('react', 'React', 'Detected the React dependency.', packageJson.path)
200
+ }
201
+ if (dependencyName === 'vite') {
202
+ register('vite', 'Vite', 'Detected the Vite build tool dependency.', packageJson.path)
203
+ }
204
+ if (dependencyName === 'hono') {
205
+ register('hono', 'Hono', 'Detected the Hono server framework dependency.', packageJson.path)
206
+ }
207
+ if (dependencyName === 'express') {
208
+ register('express', 'Express', 'Detected the Express server framework dependency.', packageJson.path)
209
+ }
210
+ if (dependencyName === '@tanstack/react-router') {
211
+ register(
212
+ 'tanstack-router',
213
+ 'TanStack Router',
214
+ 'Detected TanStack Router in package dependencies.',
215
+ packageJson.path,
216
+ )
217
+ }
218
+ if (dependencyName === 'tailwindcss') {
219
+ register('tailwindcss', 'Tailwind CSS', 'Detected Tailwind CSS in package dependencies.', packageJson.path)
220
+ }
221
+ if (dependencyName === '@better-auth/better-auth' || dependencyName === 'better-auth') {
222
+ register('better-auth', 'Better Auth', 'Detected Better Auth in package dependencies.', packageJson.path)
223
+ }
224
+ if (dependencyName === 'bullmq') {
225
+ register('bullmq', 'BullMQ', 'Detected BullMQ job processing.', packageJson.path)
226
+ }
227
+ if (dependencyName === 'prisma') {
228
+ register('prisma', 'Prisma', 'Detected Prisma ORM.', packageJson.path)
229
+ }
230
+ if (dependencyName === 'surrealdb') {
231
+ register('surrealdb', 'SurrealDB', 'Detected SurrealDB client usage.', packageJson.path)
232
+ }
233
+ }
234
+ }
235
+
236
+ return [...dependencyMap.entries()].map(([dependencyName, value]) =>
237
+ createSignal({ key: dependencyName, label: value.label, reason: value.reason, evidencePaths: value.evidencePaths }),
238
+ )
239
+ }
240
+
241
+ function inferPackageManagerSignals(filePaths: string[]): RepositoryStructureSignal[] {
242
+ const signals: RepositoryStructureSignal[] = []
243
+ if (filePaths.includes('bun.lock') || filePaths.includes('bun.lockb')) {
244
+ signals.push(
245
+ createSignal({ key: 'bun', label: 'Bun', reason: 'Detected Bun lockfile.', evidencePaths: ['bun.lock'] }),
246
+ )
247
+ }
248
+ if (filePaths.includes('package-lock.json')) {
249
+ signals.push(
250
+ createSignal({
251
+ key: 'npm',
252
+ label: 'npm',
253
+ reason: 'Detected npm lockfile.',
254
+ evidencePaths: ['package-lock.json'],
255
+ }),
256
+ )
257
+ }
258
+ if (filePaths.includes('pnpm-lock.yaml')) {
259
+ signals.push(
260
+ createSignal({
261
+ key: 'pnpm',
262
+ label: 'pnpm',
263
+ reason: 'Detected pnpm lockfile.',
264
+ evidencePaths: ['pnpm-lock.yaml'],
265
+ }),
266
+ )
267
+ }
268
+ if (filePaths.includes('yarn.lock')) {
269
+ signals.push(
270
+ createSignal({ key: 'yarn', label: 'Yarn', reason: 'Detected Yarn lockfile.', evidencePaths: ['yarn.lock'] }),
271
+ )
272
+ }
273
+ return signals
274
+ }
275
+
276
+ function inferRepoShape(params: {
277
+ topLevelEntries: string[]
278
+ components: RepositoryStructureComponent[]
279
+ frameworks: RepositoryStructureSignal[]
280
+ filePaths: string[]
281
+ }): { kind: RepositoryStructureShape; reason: string; evidencePaths: string[] } {
282
+ if (
283
+ params.topLevelEntries.includes('apps') ||
284
+ params.topLevelEntries.includes('packages') ||
285
+ params.components.length >= 3
286
+ ) {
287
+ return {
288
+ kind: 'monorepo',
289
+ reason: 'Multiple package or app roots were detected, which indicates a monorepo-style repository.',
290
+ evidencePaths: normalizeEvidencePaths(params.components.map((component) => component.path)),
291
+ }
292
+ }
293
+
294
+ if (
295
+ params.topLevelEntries.every((entry) => ['docs', 'documentation', 'website'].includes(entry)) ||
296
+ params.filePaths.every((filePath) => filePath.endsWith('.md') || filePath.startsWith('docs/'))
297
+ ) {
298
+ return {
299
+ kind: 'docs-only',
300
+ reason: 'The repository appears to be documentation-centric with little or no application source.',
301
+ evidencePaths: normalizeEvidencePaths(params.topLevelEntries),
302
+ }
303
+ }
304
+
305
+ if (params.frameworks.some((framework) => ['React', 'Next.js', 'Vite'].includes(framework.label))) {
306
+ return {
307
+ kind: 'single-project',
308
+ reason: 'A single primary web application framework was detected without strong monorepo signals.',
309
+ evidencePaths: normalizeEvidencePaths(params.frameworks.flatMap((framework) => framework.evidencePaths)),
310
+ }
311
+ }
312
+
313
+ return {
314
+ kind: 'uncertain',
315
+ reason: 'The repository structure did not clearly match a single canonical shape.',
316
+ evidencePaths: normalizeEvidencePaths(params.topLevelEntries),
317
+ }
318
+ }
319
+
320
+ function inferComponentKind(componentPath: string, packageJson: PackageJson): RepositoryStructureComponent['kind'] {
321
+ const dependencyNames = [
322
+ ...Object.keys(packageJson.dependencies ?? {}),
323
+ ...Object.keys(packageJson.devDependencies ?? {}),
324
+ ]
325
+
326
+ if (componentPath.startsWith('docs/') || componentPath === 'docs') return 'docs'
327
+ if (componentPath.startsWith('apps/') || dependencyNames.includes('next') || dependencyNames.includes('react')) {
328
+ return 'app'
329
+ }
330
+ if (
331
+ componentPath.startsWith('services/') ||
332
+ dependencyNames.includes('hono') ||
333
+ dependencyNames.includes('express')
334
+ ) {
335
+ return 'service'
336
+ }
337
+ if (componentPath.startsWith('packages/')) return 'package'
338
+ if (packageJson.private === false) return 'library'
339
+ return 'unknown'
340
+ }
341
+
342
+ function inferComponentFrameworks(packageJson: PackageJson): string[] {
343
+ return unique(
344
+ [
345
+ packageJson.dependencies?.next ? 'Next.js' : null,
346
+ packageJson.dependencies?.react ? 'React' : null,
347
+ packageJson.dependencies?.hono ? 'Hono' : null,
348
+ packageJson.dependencies?.express ? 'Express' : null,
349
+ packageJson.dependencies?.tailwindcss ? 'Tailwind CSS' : null,
350
+ ].filter((value): value is string => Boolean(value)),
351
+ )
352
+ }
353
+
354
+ function inferComponentLanguages(filePaths: string[], componentPath: string): string[] {
355
+ const componentFiles = filePaths.filter(
356
+ (filePath) => filePath === componentPath || filePath.startsWith(`${componentPath}/`),
357
+ )
358
+ return unique(
359
+ componentFiles
360
+ .map((filePath) => LANGUAGE_BY_EXTENSION[path.extname(filePath).toLowerCase()])
361
+ .filter((value): value is string => Boolean(value)),
362
+ ).slice(0, 20)
363
+ }
364
+
365
+ function summarizeSignals(signals: RepositoryStructureSignal[], limit = 20): string[] {
366
+ return truncate(unique(signals.map((signal) => signal.label)), limit)
367
+ }
368
+
369
+ function buildStructureMarkdown(params: {
370
+ repoStructure: RepositoryStructureArtifact
371
+ structureSummary: RepositoryStructureSummary
372
+ }): string {
373
+ const summary = params.structureSummary
374
+ return [
375
+ '# Repository Structure',
376
+ '',
377
+ `- Shape: ${summary.repoShape}`,
378
+ `- Package managers: ${summary.packageManagers.join(', ') || 'none detected'}`,
379
+ `- Languages: ${summary.languages.join(', ') || 'none detected'}`,
380
+ `- Frameworks: ${summary.frameworks.join(', ') || 'none detected'}`,
381
+ `- Components: ${
382
+ summary.components.length > 0
383
+ ? summary.components.map((component) => `${component.kind}:${component.name}@${component.path}`).join(', ')
384
+ : 'none detected'
385
+ }`,
386
+ `- Entry points: ${summary.entryPoints.join(', ') || 'none detected'}`,
387
+ `- Routes: ${summary.routeSignals.join(', ') || 'none detected'}`,
388
+ `- APIs: ${summary.apiSignals.join(', ') || 'none detected'}`,
389
+ `- Database: ${summary.databaseSignals.join(', ') || 'none detected'}`,
390
+ `- Auth: ${summary.authSignals.join(', ') || 'none detected'}`,
391
+ `- Jobs: ${summary.jobSignals.join(', ') || 'none detected'}`,
392
+ `- Environment files: ${summary.envFiles.join(', ') || 'none detected'}`,
393
+ `- Docker: ${summary.dockerFiles.join(', ') || 'none detected'}`,
394
+ `- CI/CD: ${summary.ciCdFiles.join(', ') || 'none detected'}`,
395
+ `- Deployment: ${summary.deploymentSignals.join(', ') || 'none detected'}`,
396
+ `- Tests: ${summary.testSignals.join(', ') || 'none detected'}`,
397
+ `- Linting: ${summary.lintSignals.join(', ') || 'none detected'}`,
398
+ `- Formatting: ${summary.formatSignals.join(', ') || 'none detected'}`,
399
+ `- Typechecking: ${summary.typecheckSignals.join(', ') || 'none detected'}`,
400
+ `- Unknowns: ${summary.unknowns.join(' | ') || 'none'}`,
401
+ ].join('\n')
402
+ }
403
+
404
+ export async function extractRepositoryStructure(params: ExtractParams): Promise<ExtractResult> {
405
+ const filePaths = await collectRelativeFilePaths(params.rootDir)
406
+ const topLevelEntries = gatherTopLevelEntries(filePaths)
407
+ const packageJsonPaths = filePaths.filter((filePath) => filePath.endsWith('package.json')).slice(0, 200)
408
+ const packageJsons = (
409
+ await Promise.all(
410
+ packageJsonPaths.map(async (packageJsonPath) => {
411
+ const data = await readPackageJson(params.rootDir, packageJsonPath)
412
+ return data ? { path: packageJsonPath, data } : null
413
+ }),
414
+ )
415
+ ).filter((value): value is { path: string; data: PackageJson } => value !== null)
416
+
417
+ const packageManagers = inferPackageManagerSignals(filePaths)
418
+ const components: RepositoryStructureComponent[] = packageJsons.map((packageJson) => {
419
+ const componentPath = packageJson.path === 'package.json' ? '.' : normalizePath(path.dirname(packageJson.path))
420
+ return {
421
+ kind: inferComponentKind(componentPath, packageJson.data),
422
+ name: packageJson.data.name?.trim() || path.basename(componentPath === '.' ? params.rootDir : componentPath),
423
+ path: componentPath,
424
+ packageManager: packageManagers[0]?.label,
425
+ languages: inferComponentLanguages(filePaths, componentPath),
426
+ frameworks: inferComponentFrameworks(packageJson.data),
427
+ reason: 'Detected a package manifest in this directory.',
428
+ evidencePaths: [packageJson.path],
429
+ }
430
+ })
431
+
432
+ const languages = gatherLanguageSignals(filePaths)
433
+ const frameworks = inferFrameworkSignals(packageJsons)
434
+ const routeSignals = buildPathSignals({
435
+ keyPrefix: 'route',
436
+ label: 'Routes',
437
+ reason: 'Detected route-related directories or files.',
438
+ filePaths: truncate(
439
+ findPaths(filePaths, (filePath) => /(^|\/)(routes|pages|app\/routes)\//.test(filePath)),
440
+ 20,
441
+ ),
442
+ })
443
+ const apiSignals = buildPathSignals({
444
+ keyPrefix: 'api',
445
+ label: 'API',
446
+ reason: 'Detected API or server route surfaces.',
447
+ filePaths: truncate(
448
+ findPaths(filePaths, (filePath) => /(^|\/)(api|server|openapi|rpc)\//.test(filePath)),
449
+ 20,
450
+ ),
451
+ })
452
+ const databaseSignals = buildPathSignals({
453
+ keyPrefix: 'database',
454
+ label: 'Database',
455
+ reason: 'Detected database schemas, migrations, or data-access directories.',
456
+ filePaths: truncate(
457
+ findPaths(filePaths, (filePath) => /(^|\/)(db|database|migrations|prisma|drizzle|schema)\b/.test(filePath)),
458
+ 20,
459
+ ),
460
+ })
461
+ const authSignals = buildPathSignals({
462
+ keyPrefix: 'auth',
463
+ label: 'Auth',
464
+ reason: 'Detected authentication-related code or configuration.',
465
+ filePaths: truncate(
466
+ findPaths(filePaths, (filePath) => /(^|\/)(auth|better-auth|clerk|next-auth)\b/.test(filePath)),
467
+ 20,
468
+ ),
469
+ })
470
+ const jobSignals = buildPathSignals({
471
+ keyPrefix: 'job',
472
+ label: 'Jobs',
473
+ reason: 'Detected jobs, queues, or worker execution surfaces.',
474
+ filePaths: truncate(
475
+ findPaths(filePaths, (filePath) => /(^|\/)(jobs|queues|workers?)\b/.test(filePath)),
476
+ 20,
477
+ ),
478
+ })
479
+ const envFiles = buildPathSignals({
480
+ keyPrefix: 'env',
481
+ label: 'Environment',
482
+ reason: 'Detected environment variable files.',
483
+ filePaths: truncate(
484
+ findPaths(filePaths, (filePath) => /(^|\/)\.env(\.|$)/.test(filePath)),
485
+ 20,
486
+ ),
487
+ })
488
+ const dockerFiles = buildPathSignals({
489
+ keyPrefix: 'docker',
490
+ label: 'Docker',
491
+ reason: 'Detected Docker build or compose files.',
492
+ filePaths: truncate(
493
+ findPaths(filePaths, (filePath) => /(Dockerfile|docker-compose|compose\.ya?ml|\.dockerignore)/i.test(filePath)),
494
+ 20,
495
+ ),
496
+ })
497
+ const ciCdFiles = buildPathSignals({
498
+ keyPrefix: 'cicd',
499
+ label: 'CI/CD',
500
+ reason: 'Detected continuous integration or delivery workflow files.',
501
+ filePaths: truncate(
502
+ findPaths(filePaths, (filePath) => /^\.github\/workflows\/|\.gitlab-ci\.yml$|circleci\//.test(filePath)),
503
+ 20,
504
+ ),
505
+ })
506
+ const deploymentFiles = buildPathSignals({
507
+ keyPrefix: 'deploy',
508
+ label: 'Deployment',
509
+ reason: 'Detected deployment platform or infrastructure descriptors.',
510
+ filePaths: truncate(
511
+ findPaths(filePaths, (filePath) =>
512
+ /(vercel\.json|fly\.toml|render\.ya?ml|netlify\.toml|railway\.json|k8s|helm)/i.test(filePath),
513
+ ),
514
+ 20,
515
+ ),
516
+ })
517
+ const testSignals = buildPathSignals({
518
+ keyPrefix: 'test',
519
+ label: 'Tests',
520
+ reason: 'Detected repository test files.',
521
+ filePaths: truncate(
522
+ findPaths(filePaths, (filePath) => /(^|\/)(__tests__|tests?)\/|(\.test\.|\.(spec)\.)/.test(filePath)),
523
+ 20,
524
+ ),
525
+ })
526
+ const lintSignals = buildPathSignals({
527
+ keyPrefix: 'lint',
528
+ label: 'Linting',
529
+ reason: 'Detected linting configuration files.',
530
+ filePaths: truncate(
531
+ findPaths(
532
+ filePaths,
533
+ (filePath) => /(eslint|oxlint|biome|lint)/i.test(filePath) && /\.(json|js|ts|mjs|cjs|yaml|yml)$/.test(filePath),
534
+ ),
535
+ 20,
536
+ ),
537
+ })
538
+ const formatSignals = buildPathSignals({
539
+ keyPrefix: 'format',
540
+ label: 'Formatting',
541
+ reason: 'Detected code-formatting configuration files.',
542
+ filePaths: truncate(
543
+ findPaths(
544
+ filePaths,
545
+ (filePath) => /(prettier|oxfmt|biome)/i.test(filePath) && /\.(json|js|ts|mjs|cjs|yaml|yml)$/.test(filePath),
546
+ ),
547
+ 20,
548
+ ),
549
+ })
550
+ const typecheckSignals = buildPathSignals({
551
+ keyPrefix: 'typecheck',
552
+ label: 'Typechecking',
553
+ reason: 'Detected typechecking configuration files.',
554
+ filePaths: truncate(
555
+ findPaths(filePaths, (filePath) => /(tsconfig|mypy|pyright)/i.test(filePath)),
556
+ 20,
557
+ ),
558
+ })
559
+ const entryPoints = buildPathSignals({
560
+ keyPrefix: 'entry',
561
+ label: 'Entry point',
562
+ reason: 'Detected likely application entry points.',
563
+ filePaths: truncate(
564
+ findPaths(filePaths, (filePath) => /(^|\/)(main|index|app|server|worker)\.(ts|tsx|js|jsx|py|go)$/.test(filePath)),
565
+ 20,
566
+ ),
567
+ })
568
+
569
+ const repoShape = inferRepoShape({ topLevelEntries, components, frameworks, filePaths })
570
+
571
+ const repoStructure = RepositoryStructureArtifactSchema.parse({
572
+ schemaVersion: 'repository-structure.v1',
573
+ extractorVersion: EXTRACTOR_VERSION,
574
+ generatedAt: new Date().toISOString(),
575
+ repository: {
576
+ repositoryKey: params.repositoryKey,
577
+ repositoryFullName: params.repositoryFullName,
578
+ defaultBranch: params.defaultBranch,
579
+ indexBranch: params.indexBranch,
580
+ commitSha: params.commitSha,
581
+ },
582
+ topLevelEntries,
583
+ repoShape,
584
+ packageManagers,
585
+ languages,
586
+ frameworks,
587
+ components: truncate(components, 200),
588
+ entryPoints,
589
+ routeSignals,
590
+ apiSignals,
591
+ databaseSignals,
592
+ authSignals,
593
+ jobSignals,
594
+ envFiles,
595
+ dockerFiles,
596
+ ciCdFiles,
597
+ deploymentFiles,
598
+ testSignals,
599
+ lintSignals,
600
+ formatSignals,
601
+ typecheckSignals,
602
+ unknowns: repoShape.kind === 'uncertain' ? ['Repository shape could not be confidently classified.'] : [],
603
+ })
604
+
605
+ const structureSummary = RepositoryStructureSummarySchema.parse({
606
+ schemaVersion: 'repository-structure-summary.v1',
607
+ extractorVersion: EXTRACTOR_VERSION,
608
+ generatedAt: repoStructure.generatedAt,
609
+ repository: repoStructure.repository,
610
+ repoShape: repoStructure.repoShape.kind,
611
+ topLevelEntries: truncate(repoStructure.topLevelEntries, 20),
612
+ packageManagers: summarizeSignals(repoStructure.packageManagers),
613
+ languages: summarizeSignals(repoStructure.languages),
614
+ frameworks: summarizeSignals(repoStructure.frameworks),
615
+ components: truncate(
616
+ repoStructure.components.map((component) => ({
617
+ kind: component.kind,
618
+ name: component.name,
619
+ path: component.path,
620
+ })),
621
+ 40,
622
+ ),
623
+ entryPoints: summarizeSignals(repoStructure.entryPoints),
624
+ routeSignals: summarizeSignals(repoStructure.routeSignals),
625
+ apiSignals: summarizeSignals(repoStructure.apiSignals),
626
+ databaseSignals: summarizeSignals(repoStructure.databaseSignals),
627
+ authSignals: summarizeSignals(repoStructure.authSignals),
628
+ jobSignals: summarizeSignals(repoStructure.jobSignals),
629
+ envFiles: summarizeSignals(repoStructure.envFiles),
630
+ dockerFiles: summarizeSignals(repoStructure.dockerFiles),
631
+ ciCdFiles: summarizeSignals(repoStructure.ciCdFiles),
632
+ testSignals: summarizeSignals(repoStructure.testSignals),
633
+ lintSignals: summarizeSignals(repoStructure.lintSignals),
634
+ formatSignals: summarizeSignals(repoStructure.formatSignals),
635
+ typecheckSignals: summarizeSignals(repoStructure.typecheckSignals),
636
+ deploymentSignals: summarizeSignals(repoStructure.deploymentFiles),
637
+ unknowns: repoStructure.unknowns.slice(0, 20),
638
+ })
639
+
640
+ return {
641
+ repoStructure,
642
+ structureSummary,
643
+ structureMarkdown: buildStructureMarkdown({ repoStructure, structureSummary }),
644
+ }
645
+ }
@@ -0,0 +1,65 @@
1
+ import os from 'node:os'
2
+
3
+ type MutableOsModule = Omit<typeof os, 'availableParallelism'> & { availableParallelism?: () => number }
4
+
5
+ const mutableOs = os as MutableOsModule
6
+
7
+ function toPositiveSafeInteger(value: unknown): number | null {
8
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null
9
+ }
10
+
11
+ function readAvailableParallelism(read?: () => unknown): number | null {
12
+ if (!read) return null
13
+
14
+ try {
15
+ return toPositiveSafeInteger(read())
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export function resolveSafeRepomixProcessConcurrency(params?: {
22
+ availableParallelism?: () => unknown
23
+ getCpuCount?: () => unknown
24
+ }): number {
25
+ const availableParallelism = readAvailableParallelism(params?.availableParallelism ?? mutableOs.availableParallelism)
26
+ if (availableParallelism) {
27
+ return availableParallelism
28
+ }
29
+
30
+ const cpuCount = toPositiveSafeInteger(params?.getCpuCount?.() ?? os.cpus().length)
31
+ if (cpuCount) {
32
+ return cpuCount
33
+ }
34
+
35
+ return 1
36
+ }
37
+
38
+ export function installSafeRepomixAvailableParallelism(): {
39
+ processConcurrency: number
40
+ patched: boolean
41
+ restore: () => void
42
+ } {
43
+ const originalAvailableParallelism = mutableOs.availableParallelism
44
+ const processConcurrency = resolveSafeRepomixProcessConcurrency()
45
+ const currentValue = readAvailableParallelism(originalAvailableParallelism)
46
+
47
+ if (currentValue === processConcurrency) {
48
+ return { processConcurrency, patched: false, restore: () => {} }
49
+ }
50
+
51
+ mutableOs.availableParallelism = () => processConcurrency
52
+
53
+ return {
54
+ processConcurrency,
55
+ patched: true,
56
+ restore: () => {
57
+ if (originalAvailableParallelism) {
58
+ mutableOs.availableParallelism = originalAvailableParallelism
59
+ return
60
+ }
61
+
62
+ delete mutableOs.availableParallelism
63
+ },
64
+ }
65
+ }