clanka 0.2.19 → 0.2.21
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.d.ts.map +1 -1
- package/dist/Agent.js +2 -1
- package/dist/Agent.js.map +1 -1
- package/dist/CodeChunker.d.ts +4 -0
- package/dist/CodeChunker.d.ts.map +1 -1
- package/dist/CodeChunker.js +64 -14
- package/dist/CodeChunker.js.map +1 -1
- package/dist/CodeChunker.test.js +71 -0
- package/dist/CodeChunker.test.js.map +1 -1
- package/dist/ScriptPreprocessing.d.ts.map +1 -1
- package/dist/ScriptPreprocessing.js +93 -0
- package/dist/ScriptPreprocessing.js.map +1 -1
- package/dist/ScriptPreprocessing.test.js +1 -0
- package/dist/ScriptPreprocessing.test.js.map +1 -1
- package/dist/SemanticSearch.d.ts +1 -0
- package/dist/SemanticSearch.d.ts.map +1 -1
- package/dist/SemanticSearch.js +4 -2
- package/dist/SemanticSearch.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +115 -0
- package/dist/cli.js.map +1 -0
- package/package.json +13 -10
- package/src/Agent.ts +2 -1
- package/src/CodeChunker.test.ts +77 -0
- package/src/CodeChunker.ts +105 -19
- package/src/ScriptPreprocessing.test.ts +1 -0
- package/src/ScriptPreprocessing.ts +134 -0
- package/src/SemanticSearch.ts +7 -2
- package/src/cli.ts +169 -0
- package/src/fixtures/patch12-broken.txt +16 -0
- package/src/fixtures/patch12-fixed.txt +16 -0
package/src/CodeChunker.ts
CHANGED
|
@@ -59,18 +59,21 @@ export class CodeChunker extends ServiceMap.Service<
|
|
|
59
59
|
readonly path: string
|
|
60
60
|
readonly chunkSize: number
|
|
61
61
|
readonly chunkOverlap: number
|
|
62
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
62
63
|
}): Effect.Effect<ReadonlyArray<CodeChunk>>
|
|
63
64
|
chunkFiles(options: {
|
|
64
65
|
readonly root: string
|
|
65
66
|
readonly paths: ReadonlyArray<string>
|
|
66
67
|
readonly chunkSize: number
|
|
67
68
|
readonly chunkOverlap: number
|
|
69
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
68
70
|
}): Stream.Stream<CodeChunk>
|
|
69
71
|
chunkCodebase(options: {
|
|
70
72
|
readonly root: string
|
|
71
73
|
readonly maxFileSize?: string | undefined
|
|
72
74
|
readonly chunkSize: number
|
|
73
75
|
readonly chunkOverlap: number
|
|
76
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
74
77
|
}): Stream.Stream<CodeChunk>
|
|
75
78
|
}
|
|
76
79
|
>()("clanka/CodeChunker") {}
|
|
@@ -157,6 +160,7 @@ interface LineRange {
|
|
|
157
160
|
interface ChunkSettings {
|
|
158
161
|
readonly chunkSize: number
|
|
159
162
|
readonly chunkOverlap: number
|
|
163
|
+
readonly chunkMaxCharacters: number
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
interface ChunkRange extends LineRange {
|
|
@@ -234,16 +238,23 @@ export const isMeaningfulFile = (path: string): boolean => {
|
|
|
234
238
|
const resolveChunkSettings = (options: {
|
|
235
239
|
readonly chunkSize: number
|
|
236
240
|
readonly chunkOverlap: number
|
|
241
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
237
242
|
}): ChunkSettings => {
|
|
238
243
|
const chunkSize = Math.max(1, options.chunkSize)
|
|
239
244
|
const chunkOverlap = Math.max(
|
|
240
245
|
0,
|
|
241
246
|
Math.min(chunkSize - 1, options.chunkOverlap),
|
|
242
247
|
)
|
|
248
|
+
const chunkMaxCharacters =
|
|
249
|
+
options.chunkMaxCharacters !== undefined &&
|
|
250
|
+
Number.isFinite(options.chunkMaxCharacters)
|
|
251
|
+
? Math.max(1, Math.floor(options.chunkMaxCharacters))
|
|
252
|
+
: Number.POSITIVE_INFINITY
|
|
243
253
|
|
|
244
254
|
return {
|
|
245
255
|
chunkSize,
|
|
246
256
|
chunkOverlap,
|
|
257
|
+
chunkMaxCharacters,
|
|
247
258
|
}
|
|
248
259
|
}
|
|
249
260
|
|
|
@@ -345,24 +356,76 @@ const normalizeLineRange = (
|
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
358
|
|
|
359
|
+
const lineLengthPrefixSums = (
|
|
360
|
+
lines: ReadonlyArray<string>,
|
|
361
|
+
): ReadonlyArray<number> => {
|
|
362
|
+
const sums = [0] as Array<number>
|
|
363
|
+
|
|
364
|
+
for (let index = 0; index < lines.length; index++) {
|
|
365
|
+
sums.push(sums[index]! + lines[index]!.length)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return sums
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lineRangeCharacterLength = (
|
|
372
|
+
prefixSums: ReadonlyArray<number>,
|
|
373
|
+
range: LineRange,
|
|
374
|
+
): number =>
|
|
375
|
+
prefixSums[range.endLine]! -
|
|
376
|
+
prefixSums[range.startLine - 1]! +
|
|
377
|
+
(range.endLine - range.startLine)
|
|
378
|
+
|
|
379
|
+
const resolveSegmentEndLine = (options: {
|
|
380
|
+
readonly startLine: number
|
|
381
|
+
readonly maxEndLine: number
|
|
382
|
+
readonly settings: ChunkSettings
|
|
383
|
+
readonly prefixSums: ReadonlyArray<number>
|
|
384
|
+
}): number => {
|
|
385
|
+
if (options.settings.chunkMaxCharacters === Number.POSITIVE_INFINITY) {
|
|
386
|
+
return options.maxEndLine
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let endLine = options.maxEndLine
|
|
390
|
+
while (
|
|
391
|
+
endLine > options.startLine &&
|
|
392
|
+
lineRangeCharacterLength(options.prefixSums, {
|
|
393
|
+
startLine: options.startLine,
|
|
394
|
+
endLine,
|
|
395
|
+
}) > options.settings.chunkMaxCharacters
|
|
396
|
+
) {
|
|
397
|
+
endLine--
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return endLine
|
|
401
|
+
}
|
|
402
|
+
|
|
348
403
|
const splitRange = (
|
|
349
404
|
range: LineRange,
|
|
350
405
|
settings: ChunkSettings,
|
|
406
|
+
prefixSums: ReadonlyArray<number>,
|
|
351
407
|
): ReadonlyArray<LineRange> => {
|
|
352
408
|
const lineCount = range.endLine - range.startLine + 1
|
|
353
|
-
if (
|
|
409
|
+
if (
|
|
410
|
+
lineCount <= settings.chunkSize &&
|
|
411
|
+
lineRangeCharacterLength(prefixSums, range) <= settings.chunkMaxCharacters
|
|
412
|
+
) {
|
|
354
413
|
return [range]
|
|
355
414
|
}
|
|
356
|
-
|
|
357
|
-
const step = settings.chunkSize - settings.chunkOverlap
|
|
358
415
|
const out = [] as Array<LineRange>
|
|
359
416
|
|
|
360
|
-
for (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const endLine =
|
|
417
|
+
for (let startLine = range.startLine; startLine <= range.endLine; ) {
|
|
418
|
+
const maxEndLine = Math.min(
|
|
419
|
+
range.endLine,
|
|
420
|
+
startLine + settings.chunkSize - 1,
|
|
421
|
+
)
|
|
422
|
+
const endLine = resolveSegmentEndLine({
|
|
423
|
+
startLine,
|
|
424
|
+
maxEndLine,
|
|
425
|
+
settings,
|
|
426
|
+
prefixSums,
|
|
427
|
+
})
|
|
428
|
+
|
|
366
429
|
out.push({
|
|
367
430
|
startLine,
|
|
368
431
|
endLine,
|
|
@@ -371,6 +434,8 @@ const splitRange = (
|
|
|
371
434
|
if (endLine >= range.endLine) {
|
|
372
435
|
break
|
|
373
436
|
}
|
|
437
|
+
|
|
438
|
+
startLine = Math.max(startLine + 1, endLine - settings.chunkOverlap + 1)
|
|
374
439
|
}
|
|
375
440
|
|
|
376
441
|
return out
|
|
@@ -648,6 +713,7 @@ const chunksFromRanges = (
|
|
|
648
713
|
|
|
649
714
|
const out = [] as Array<CodeChunk>
|
|
650
715
|
const seen = new Set<string>()
|
|
716
|
+
const prefixSums = lineLengthPrefixSums(lines)
|
|
651
717
|
|
|
652
718
|
for (const range of ranges) {
|
|
653
719
|
const normalizedRange = normalizeLineRange(range, lines.length)
|
|
@@ -655,7 +721,7 @@ const chunksFromRanges = (
|
|
|
655
721
|
continue
|
|
656
722
|
}
|
|
657
723
|
|
|
658
|
-
const allSegments = splitRange(normalizedRange, settings)
|
|
724
|
+
const allSegments = splitRange(normalizedRange, settings, prefixSums)
|
|
659
725
|
const segments =
|
|
660
726
|
range.type === "class" &&
|
|
661
727
|
allSegments.length > 1 &&
|
|
@@ -709,8 +775,8 @@ const chunkWithLineWindows = (
|
|
|
709
775
|
lines: ReadonlyArray<string>,
|
|
710
776
|
settings: ChunkSettings,
|
|
711
777
|
): ReadonlyArray<CodeChunk> => {
|
|
712
|
-
const step = settings.chunkSize - settings.chunkOverlap
|
|
713
778
|
const out = [] as Array<CodeChunk>
|
|
779
|
+
const prefixSums = lineLengthPrefixSums(lines)
|
|
714
780
|
|
|
715
781
|
for (let index = 0; index < lines.length; ) {
|
|
716
782
|
if (!isMeaningfulLine(lines[index]!)) {
|
|
@@ -718,25 +784,38 @@ const chunkWithLineWindows = (
|
|
|
718
784
|
continue
|
|
719
785
|
}
|
|
720
786
|
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
787
|
+
const startLine = index + 1
|
|
788
|
+
const maxEndLine = Math.min(
|
|
789
|
+
lines.length,
|
|
790
|
+
startLine + settings.chunkSize - 1,
|
|
791
|
+
)
|
|
792
|
+
const endLine = resolveSegmentEndLine({
|
|
793
|
+
startLine,
|
|
794
|
+
maxEndLine,
|
|
795
|
+
settings,
|
|
796
|
+
prefixSums,
|
|
797
|
+
})
|
|
798
|
+
const chunkLines = lines.slice(startLine - 1, endLine)
|
|
724
799
|
|
|
725
800
|
out.push({
|
|
726
801
|
path,
|
|
727
|
-
startLine
|
|
728
|
-
endLine
|
|
802
|
+
startLine,
|
|
803
|
+
endLine,
|
|
729
804
|
name: undefined,
|
|
730
805
|
type: undefined,
|
|
731
806
|
parent: undefined,
|
|
732
807
|
content: chunkLines.join("\n"),
|
|
733
808
|
})
|
|
734
809
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (end >= lines.length) {
|
|
810
|
+
if (endLine >= lines.length) {
|
|
738
811
|
break
|
|
739
812
|
}
|
|
813
|
+
|
|
814
|
+
const nextStartLine = Math.max(
|
|
815
|
+
startLine + 1,
|
|
816
|
+
endLine - settings.chunkOverlap + 1,
|
|
817
|
+
)
|
|
818
|
+
index = nextStartLine - 1
|
|
740
819
|
}
|
|
741
820
|
|
|
742
821
|
return out
|
|
@@ -752,6 +831,7 @@ export const chunkFileContent = (
|
|
|
752
831
|
options: {
|
|
753
832
|
readonly chunkSize: number
|
|
754
833
|
readonly chunkOverlap: number
|
|
834
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
755
835
|
},
|
|
756
836
|
): ReadonlyArray<CodeChunk> => {
|
|
757
837
|
if (content.trim().length === 0 || isProbablyMinified(content)) {
|
|
@@ -869,6 +949,9 @@ export const layer: Layer.Layer<
|
|
|
869
949
|
path,
|
|
870
950
|
chunkSize: options.chunkSize,
|
|
871
951
|
chunkOverlap: options.chunkOverlap,
|
|
952
|
+
...(options.chunkMaxCharacters === undefined
|
|
953
|
+
? {}
|
|
954
|
+
: { chunkMaxCharacters: options.chunkMaxCharacters }),
|
|
872
955
|
}),
|
|
873
956
|
Stream.fromArrayEffect,
|
|
874
957
|
),
|
|
@@ -891,6 +974,9 @@ export const layer: Layer.Layer<
|
|
|
891
974
|
paths: files,
|
|
892
975
|
chunkSize: options.chunkSize,
|
|
893
976
|
chunkOverlap: options.chunkOverlap,
|
|
977
|
+
...(options.chunkMaxCharacters === undefined
|
|
978
|
+
? {}
|
|
979
|
+
: { chunkMaxCharacters: options.chunkMaxCharacters }),
|
|
894
980
|
})
|
|
895
981
|
}, Stream.unwrap)
|
|
896
982
|
|
|
@@ -316,6 +316,127 @@ const findClosingParen = (text: string, openParen: number): number => {
|
|
|
316
316
|
return -1
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
const findClosingBrace = (text: string, openBrace: number): number => {
|
|
320
|
+
let depth = 1
|
|
321
|
+
let stringDelimiter: '"' | "'" | "`" | undefined
|
|
322
|
+
|
|
323
|
+
for (let i = openBrace + 1; i < text.length; i++) {
|
|
324
|
+
const char = text[i]!
|
|
325
|
+
|
|
326
|
+
if (stringDelimiter !== undefined) {
|
|
327
|
+
if (char === stringDelimiter && !isEscaped(text, i)) {
|
|
328
|
+
stringDelimiter = undefined
|
|
329
|
+
}
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
334
|
+
stringDelimiter = char
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (char === "{") {
|
|
339
|
+
depth++
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (char === "}") {
|
|
344
|
+
depth--
|
|
345
|
+
if (depth === 0) {
|
|
346
|
+
return i
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return -1
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fixObjectLiteralTemplateValues = (text: string): string =>
|
|
355
|
+
text.replace(/\\{2,}(?=`|\$\{)/g, "\\")
|
|
356
|
+
|
|
357
|
+
const fixAssignedObjectTemplateValues = (
|
|
358
|
+
script: string,
|
|
359
|
+
variableName: string,
|
|
360
|
+
): string => {
|
|
361
|
+
let out = script
|
|
362
|
+
let cursor = 0
|
|
363
|
+
|
|
364
|
+
while (cursor < out.length) {
|
|
365
|
+
const variableStart = findNextIdentifier(out, variableName, cursor)
|
|
366
|
+
if (variableStart === -1) {
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let assignmentStart = skipWhitespace(
|
|
371
|
+
out,
|
|
372
|
+
variableStart + variableName.length,
|
|
373
|
+
)
|
|
374
|
+
if (out[assignmentStart] === ":") {
|
|
375
|
+
assignmentStart = findTypeAnnotationAssignment(out, assignmentStart + 1)
|
|
376
|
+
if (assignmentStart === -1) {
|
|
377
|
+
cursor = variableStart + variableName.length
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
out[assignmentStart] !== "=" ||
|
|
384
|
+
out[assignmentStart + 1] === "=" ||
|
|
385
|
+
out[assignmentStart + 1] === ">"
|
|
386
|
+
) {
|
|
387
|
+
cursor = variableStart + variableName.length
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const objectStart = skipWhitespace(out, assignmentStart + 1)
|
|
392
|
+
if (out[objectStart] !== "{") {
|
|
393
|
+
cursor = objectStart + 1
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const objectEnd = findClosingBrace(out, objectStart)
|
|
398
|
+
if (objectEnd === -1) {
|
|
399
|
+
cursor = objectStart + 1
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const original = out.slice(objectStart, objectEnd + 1)
|
|
404
|
+
const escaped = fixObjectLiteralTemplateValues(original)
|
|
405
|
+
if (escaped !== original) {
|
|
406
|
+
out = `${out.slice(0, objectStart)}${escaped}${out.slice(objectEnd + 1)}`
|
|
407
|
+
cursor = objectEnd + (escaped.length - original.length) + 1
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
cursor = objectEnd + 1
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return out
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const escapeRegExp = (text: string): string =>
|
|
418
|
+
text.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")
|
|
419
|
+
|
|
420
|
+
const collectObjectEntryMapSources = (
|
|
421
|
+
script: string,
|
|
422
|
+
valueIdentifier: string,
|
|
423
|
+
): ReadonlySet<string> => {
|
|
424
|
+
const out = new Set<string>()
|
|
425
|
+
const pattern = new RegExp(
|
|
426
|
+
`Object\\.entries\\(\\s*([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\)\\s*\\.map\\(\\s*(?:async\\s*)?\\(\\s*\\[\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*,\\s*${escapeRegExp(valueIdentifier)}\\s*\\]\\s*\\)\\s*=>`,
|
|
427
|
+
"g",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
for (const match of script.matchAll(pattern)) {
|
|
431
|
+
const sourceIdentifier = match[1]
|
|
432
|
+
if (sourceIdentifier !== undefined) {
|
|
433
|
+
out.add(sourceIdentifier)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return out
|
|
438
|
+
}
|
|
439
|
+
|
|
319
440
|
const findCallTemplateEnd = (
|
|
320
441
|
text: string,
|
|
321
442
|
templateStart: number,
|
|
@@ -641,10 +762,23 @@ const fixAssignedTemplatesForToolCalls = (script: string): string => {
|
|
|
641
762
|
identifiers.add("patch")
|
|
642
763
|
}
|
|
643
764
|
|
|
765
|
+
const objectTemplateIdentifiers = new Set<string>()
|
|
766
|
+
for (const identifier of identifiers) {
|
|
767
|
+
for (const sourceIdentifier of collectObjectEntryMapSources(
|
|
768
|
+
script,
|
|
769
|
+
identifier,
|
|
770
|
+
)) {
|
|
771
|
+
objectTemplateIdentifiers.add(sourceIdentifier)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
644
775
|
let out = script
|
|
645
776
|
for (const identifier of identifiers) {
|
|
646
777
|
out = fixAssignedTemplate(out, identifier)
|
|
647
778
|
}
|
|
779
|
+
for (const identifier of objectTemplateIdentifiers) {
|
|
780
|
+
out = fixAssignedObjectTemplateValues(out, identifier)
|
|
781
|
+
}
|
|
648
782
|
return out
|
|
649
783
|
}
|
|
650
784
|
|
package/src/SemanticSearch.ts
CHANGED
|
@@ -42,10 +42,13 @@ export class SemanticSearch extends ServiceMap.Service<
|
|
|
42
42
|
|
|
43
43
|
const normalizePath = (path: string) => path.replace(/\\/g, "/")
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const resolveChunkConfig = (options: {
|
|
46
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
47
|
+
}) => ({
|
|
46
48
|
chunkSize: 30,
|
|
47
49
|
chunkOverlap: 0,
|
|
48
|
-
|
|
50
|
+
chunkMaxCharacters: options.chunkMaxCharacters ?? 10_000,
|
|
51
|
+
})
|
|
49
52
|
|
|
50
53
|
export const makeEmbeddingResolver = (
|
|
51
54
|
resolver: EmbeddingModel.Service["resolver"],
|
|
@@ -100,6 +103,7 @@ export const layer = (options: {
|
|
|
100
103
|
readonly embeddingBatchSize?: number | undefined
|
|
101
104
|
readonly embeddingRequestDelay?: Duration.Input | undefined
|
|
102
105
|
readonly concurrency?: number | undefined
|
|
106
|
+
readonly chunkMaxCharacters?: number | undefined
|
|
103
107
|
}): Layer.Layer<
|
|
104
108
|
SemanticSearch,
|
|
105
109
|
| SqlError.SqlError
|
|
@@ -121,6 +125,7 @@ export const layer = (options: {
|
|
|
121
125
|
const root = pathService.resolve(options.directory)
|
|
122
126
|
const resolver = makeEmbeddingResolver(embeddings.resolver, options)
|
|
123
127
|
const concurrency = options.concurrency ?? 2000
|
|
128
|
+
const chunkConfig = resolveChunkConfig(options)
|
|
124
129
|
const indexHandle = yield* FiberHandle.make()
|
|
125
130
|
const console = yield* Console.Console
|
|
126
131
|
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Prompt from "effect/unstable/cli/Prompt"
|
|
4
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
|
|
5
|
+
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
6
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
|
7
|
+
import * as NodeSocket from "@effect/platform-node/NodeSocket"
|
|
8
|
+
import * as Codex from "./Codex.ts"
|
|
9
|
+
import * as Copilot from "./Copilot.ts"
|
|
10
|
+
import * as Agent from "./Agent.ts"
|
|
11
|
+
import * as Stream from "effect/Stream"
|
|
12
|
+
import * as OutputFormatter from "./OutputFormatter.ts"
|
|
13
|
+
import * as Stdio from "effect/Stdio"
|
|
14
|
+
import { pipe } from "effect/Function"
|
|
15
|
+
import * as Layer from "effect/Layer"
|
|
16
|
+
import * as Path from "effect/Path"
|
|
17
|
+
import * as Config from "effect/Config"
|
|
18
|
+
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
19
|
+
import * as SemanticSearch from "./SemanticSearch.ts"
|
|
20
|
+
import * as Option from "effect/Option"
|
|
21
|
+
import { OpenAiClient, OpenAiEmbeddingModel } from "@effect/ai-openai"
|
|
22
|
+
|
|
23
|
+
const Kvs = Layer.unwrap(
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const path = yield* Path.Path
|
|
26
|
+
|
|
27
|
+
const configHome = yield* Config.nonEmptyString("XDG_CONFIG_HOME").pipe(
|
|
28
|
+
Config.orElse(() =>
|
|
29
|
+
Config.nonEmptyString("HOME").pipe(
|
|
30
|
+
Config.map((home) => path.join(home, ".config")),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
return KeyValueStore.layerFileSystem(path.join(configHome, "clanka"))
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Layer.provide(NodeServices.layer))
|
|
37
|
+
|
|
38
|
+
const Search = Layer.unwrap(
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const apiKey = yield* Config.redacted("OPENAI_API_KEY").pipe(Config.option)
|
|
41
|
+
|
|
42
|
+
if (Option.isNone(apiKey)) {
|
|
43
|
+
yield* Effect.logWarning("OPENAI_API_KEY is not set")
|
|
44
|
+
return Layer.empty
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const path = yield* Path.Path
|
|
48
|
+
|
|
49
|
+
return SemanticSearch.layer({
|
|
50
|
+
directory: process.cwd(),
|
|
51
|
+
database: path.join(".clanka", "search.sqlite"),
|
|
52
|
+
}).pipe(
|
|
53
|
+
Layer.provide(
|
|
54
|
+
OpenAiEmbeddingModel.model("text-embedding-3-small", {
|
|
55
|
+
dimensions: 1536,
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
Layer.provide(
|
|
59
|
+
OpenAiClient.layer({
|
|
60
|
+
apiKey: apiKey.value,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
}),
|
|
65
|
+
).pipe(Layer.provide([NodeServices.layer, NodeHttpClient.layerUndici]))
|
|
66
|
+
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const stdio = yield* Stdio.Stdio
|
|
69
|
+
|
|
70
|
+
const provider = yield* Prompt.select({
|
|
71
|
+
message: "Select a provider",
|
|
72
|
+
choices: [
|
|
73
|
+
{
|
|
74
|
+
title: "openai",
|
|
75
|
+
value: "openai",
|
|
76
|
+
selected: true,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: "copilot",
|
|
80
|
+
value: "copilot",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
const modelRaw = yield* Prompt.text({
|
|
85
|
+
message: "Enter a model",
|
|
86
|
+
default: "gpt-5.4/medium",
|
|
87
|
+
validate(value) {
|
|
88
|
+
const parts = value.split("/")
|
|
89
|
+
if (parts.length !== 2) {
|
|
90
|
+
return Effect.fail("Invalid model")
|
|
91
|
+
}
|
|
92
|
+
return Effect.succeed(value)
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
const semantic = yield* Prompt.confirm({
|
|
96
|
+
message: "Use semantic search? (uses OPENAI_API_KEY env var)",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const [model, reasoning] = modelRaw.split("/") as [string, string]
|
|
100
|
+
const Model =
|
|
101
|
+
provider === "openai"
|
|
102
|
+
? Codex.modelWebSocket(model, {
|
|
103
|
+
reasoning: {
|
|
104
|
+
effort: reasoning as any,
|
|
105
|
+
},
|
|
106
|
+
}).pipe(
|
|
107
|
+
Layer.merge(
|
|
108
|
+
Agent.layerSubagentModel(
|
|
109
|
+
Codex.modelWebSocket("gpt-5.4-mini", {
|
|
110
|
+
reasoning: {
|
|
111
|
+
effort: "high",
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
Layer.provide(Codex.layerClient),
|
|
117
|
+
)
|
|
118
|
+
: Copilot.model(model, {
|
|
119
|
+
reasoning: {
|
|
120
|
+
effort: reasoning,
|
|
121
|
+
},
|
|
122
|
+
}).pipe(
|
|
123
|
+
Layer.merge(
|
|
124
|
+
Agent.layerSubagentModel(
|
|
125
|
+
Copilot.model(model, {
|
|
126
|
+
reasoning: {
|
|
127
|
+
effort: "medium",
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
Layer.provide(Copilot.layerClient),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return yield* Effect.gen(function* () {
|
|
136
|
+
const agent = yield* Agent.Agent
|
|
137
|
+
|
|
138
|
+
while (true) {
|
|
139
|
+
const prompt = yield* Prompt.text({
|
|
140
|
+
message: ">",
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
yield* pipe(
|
|
144
|
+
agent.send({ prompt }),
|
|
145
|
+
Stream.unwrap,
|
|
146
|
+
OutputFormatter.pretty({ outputTruncation: 30 }),
|
|
147
|
+
Stream.run(stdio.stdout()),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
console.log("")
|
|
151
|
+
}
|
|
152
|
+
}).pipe(
|
|
153
|
+
Effect.provide([
|
|
154
|
+
Agent.layerLocal({
|
|
155
|
+
directory: process.cwd(),
|
|
156
|
+
}),
|
|
157
|
+
Model,
|
|
158
|
+
semantic ? Search : Layer.empty,
|
|
159
|
+
]),
|
|
160
|
+
)
|
|
161
|
+
}).pipe(
|
|
162
|
+
Effect.provide([
|
|
163
|
+
NodeServices.layer,
|
|
164
|
+
Kvs,
|
|
165
|
+
NodeHttpClient.layerUndici,
|
|
166
|
+
NodeSocket.layerWebSocketConstructorWS,
|
|
167
|
+
]),
|
|
168
|
+
NodeRuntime.runMain,
|
|
169
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const files = {
|
|
2
|
+
'.changeset/green-seahorses-jump.md': `---\n"effect": minor\n---\n\nMigrate unstable \\`SqlError\\` to a reason-based shape (\\`new SqlError({ reason })\\`) with structured reason classes and retryable/classified reasons.`,
|
|
3
|
+
'.changeset/cool-islands-move.md': `---\n"@effect/sql-pg": minor\n---\n\nClassify PostgreSQL native/SQLSTATE failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
4
|
+
'.changeset/curly-buckets-argue.md': `---\n"@effect/sql-mysql2": minor\n---\n\nClassify mysql2 native errno failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
5
|
+
'.changeset/green-maps-think.md': `---\n"@effect/sql-mssql": minor\n---\n\nClassify MSSQL native error-number failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
6
|
+
'.changeset/loud-gorillas-grab.md': `---\n"@effect/sql-sqlite-node": minor\n---\n\nClassify sqlite-node native SQLite failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
7
|
+
'.changeset/late-brooms-decide.md': `---\n"@effect/sql-sqlite-bun": minor\n---\n\nClassify sqlite-bun native SQLite failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
8
|
+
'.changeset/giant-dryers-sing.md': `---\n"@effect/sql-sqlite-wasm": minor\n---\n\nClassify sqlite-wasm native SQLite failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
9
|
+
'.changeset/nice-walls-pull.md': `---\n"@effect/sql-sqlite-do": minor\n---\n\nClassify durable-object SQLite failures into structured \\`SqlError\\` reasons with Unknown fallback and migrate driver construction to reason-based \\`SqlError\\`.`,
|
|
10
|
+
'.changeset/flat-spoons-chew.md': `---\n"@effect/sql-sqlite-react-native": minor\n---\n\nClassify react-native SQLite failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`,
|
|
11
|
+
'.changeset/metal-geese-fly.md': `---\n"@effect/sql-d1": minor\n---\n\nMigrate D1 driver \\`SqlError\\` construction to the reason-based shape and classify native failures into structured reasons with Unknown fallback when SQLite codes are unavailable.`,
|
|
12
|
+
'.changeset/clean-lions-hear.md': `---\n"@effect/sql-libsql": minor\n---\n\nClassify libSQL native failures into structured \\`SqlError\\` reasons (using SQLite code mapping where available) and migrate driver construction to reason-based \\`SqlError\\`.`,
|
|
13
|
+
'.changeset/fresh-bears-trace.md': `---\n"@effect/sql-clickhouse": minor\n---\n\nClassify ClickHouse native failures into structured \\`SqlError\\` reasons and migrate driver construction to the reason-based \\`SqlError\\` shape.`
|
|
14
|
+
}
|
|
15
|
+
await Promise.all(Object.entries(files).map(([path, content]) => writeFile({ path, content })))
|
|
16
|
+
console.log('wrote', Object.keys(files).length, 'changesets')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const files = {
|
|
2
|
+
'.changeset/green-seahorses-jump.md': `---\n"effect": minor\n---\n\nMigrate unstable \`SqlError\` to a reason-based shape (\`new SqlError({ reason })\`) with structured reason classes and retryable/classified reasons.`,
|
|
3
|
+
'.changeset/cool-islands-move.md': `---\n"@effect/sql-pg": minor\n---\n\nClassify PostgreSQL native/SQLSTATE failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
4
|
+
'.changeset/curly-buckets-argue.md': `---\n"@effect/sql-mysql2": minor\n---\n\nClassify mysql2 native errno failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
5
|
+
'.changeset/green-maps-think.md': `---\n"@effect/sql-mssql": minor\n---\n\nClassify MSSQL native error-number failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
6
|
+
'.changeset/loud-gorillas-grab.md': `---\n"@effect/sql-sqlite-node": minor\n---\n\nClassify sqlite-node native SQLite failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
7
|
+
'.changeset/late-brooms-decide.md': `---\n"@effect/sql-sqlite-bun": minor\n---\n\nClassify sqlite-bun native SQLite failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
8
|
+
'.changeset/giant-dryers-sing.md': `---\n"@effect/sql-sqlite-wasm": minor\n---\n\nClassify sqlite-wasm native SQLite failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
9
|
+
'.changeset/nice-walls-pull.md': `---\n"@effect/sql-sqlite-do": minor\n---\n\nClassify durable-object SQLite failures into structured \`SqlError\` reasons with Unknown fallback and migrate driver construction to reason-based \`SqlError\`.`,
|
|
10
|
+
'.changeset/flat-spoons-chew.md': `---\n"@effect/sql-sqlite-react-native": minor\n---\n\nClassify react-native SQLite failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`,
|
|
11
|
+
'.changeset/metal-geese-fly.md': `---\n"@effect/sql-d1": minor\n---\n\nMigrate D1 driver \`SqlError\` construction to the reason-based shape and classify native failures into structured reasons with Unknown fallback when SQLite codes are unavailable.`,
|
|
12
|
+
'.changeset/clean-lions-hear.md': `---\n"@effect/sql-libsql": minor\n---\n\nClassify libSQL native failures into structured \`SqlError\` reasons (using SQLite code mapping where available) and migrate driver construction to reason-based \`SqlError\`.`,
|
|
13
|
+
'.changeset/fresh-bears-trace.md': `---\n"@effect/sql-clickhouse": minor\n---\n\nClassify ClickHouse native failures into structured \`SqlError\` reasons and migrate driver construction to the reason-based \`SqlError\` shape.`
|
|
14
|
+
}
|
|
15
|
+
await Promise.all(Object.entries(files).map(([path, content]) => writeFile({ path, content })))
|
|
16
|
+
console.log('wrote', Object.keys(files).length, 'changesets')
|