shark-ai 0.3.18 → 0.4.1
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/bin/shark.js +1075 -620
- package/dist/bin/shark.js.map +1 -1
- package/package.json +3 -2
package/dist/bin/shark.js
CHANGED
|
@@ -417,7 +417,35 @@ var sseClient = new SSEClient();
|
|
|
417
417
|
// src/core/agents/agent-response-parser.ts
|
|
418
418
|
import { z as z2 } from "zod";
|
|
419
419
|
var AgentActionSchema = z2.object({
|
|
420
|
-
type: z2.enum([
|
|
420
|
+
type: z2.enum([
|
|
421
|
+
"create_file",
|
|
422
|
+
"modify_file",
|
|
423
|
+
"list_files",
|
|
424
|
+
"search_file",
|
|
425
|
+
"read_file",
|
|
426
|
+
"delete_file",
|
|
427
|
+
"list_structure",
|
|
428
|
+
"modify_ast",
|
|
429
|
+
"search_ast",
|
|
430
|
+
"run_command",
|
|
431
|
+
"talk_with_user",
|
|
432
|
+
"ast_list_structure",
|
|
433
|
+
"ast_get_method",
|
|
434
|
+
"ast_add_method",
|
|
435
|
+
"ast_modify_method",
|
|
436
|
+
"ast_remove_method",
|
|
437
|
+
"ast_add_class",
|
|
438
|
+
"ast_add_property",
|
|
439
|
+
"ast_remove_property",
|
|
440
|
+
"ast_add_decorator",
|
|
441
|
+
"ast_add_interface",
|
|
442
|
+
"ast_add_type_alias",
|
|
443
|
+
"ast_add_function",
|
|
444
|
+
"ast_remove_function",
|
|
445
|
+
"ast_add_import",
|
|
446
|
+
"ast_remove_import",
|
|
447
|
+
"ast_organize_imports"
|
|
448
|
+
]),
|
|
421
449
|
path: z2.string().nullable().optional(),
|
|
422
450
|
// Nullable for strict mode combatibility
|
|
423
451
|
content: z2.string().nullable().optional(),
|
|
@@ -433,6 +461,22 @@ var AgentActionSchema = z2.object({
|
|
|
433
461
|
language: z2.string().nullable().optional(),
|
|
434
462
|
file_path: z2.string().nullable().optional(),
|
|
435
463
|
// Alias for path in ast-grep actions
|
|
464
|
+
// New AST Tool Specific Fields
|
|
465
|
+
class_name: z2.string().nullable().optional(),
|
|
466
|
+
method_name: z2.string().nullable().optional(),
|
|
467
|
+
method_code: z2.string().nullable().optional(),
|
|
468
|
+
property_name: z2.string().nullable().optional(),
|
|
469
|
+
property_code: z2.string().nullable().optional(),
|
|
470
|
+
extends_class: z2.string().nullable().optional(),
|
|
471
|
+
implements_interfaces: z2.array(z2.string()).nullable().optional(),
|
|
472
|
+
decorator_code: z2.string().nullable().optional(),
|
|
473
|
+
interface_code: z2.string().nullable().optional(),
|
|
474
|
+
type_code: z2.string().nullable().optional(),
|
|
475
|
+
function_name: z2.string().nullable().optional(),
|
|
476
|
+
function_code: z2.string().nullable().optional(),
|
|
477
|
+
import_statement: z2.string().nullable().optional(),
|
|
478
|
+
module_path: z2.string().nullable().optional(),
|
|
479
|
+
new_body: z2.string().nullable().optional(),
|
|
436
480
|
// Preview confirmation
|
|
437
481
|
confirmed: z2.boolean().nullable().optional()
|
|
438
482
|
});
|
|
@@ -786,142 +830,413 @@ async function interactiveBusinessAnalyst() {
|
|
|
786
830
|
}
|
|
787
831
|
|
|
788
832
|
// src/core/agents/specification-agent.ts
|
|
789
|
-
import
|
|
790
|
-
import
|
|
833
|
+
import fs5 from "fs";
|
|
834
|
+
import path6 from "path";
|
|
791
835
|
|
|
792
836
|
// src/core/agents/agent-tools.ts
|
|
793
|
-
import
|
|
794
|
-
import
|
|
837
|
+
import fs4 from "fs";
|
|
838
|
+
import path5 from "path";
|
|
795
839
|
import fg from "fast-glob";
|
|
796
840
|
import { exec } from "child_process";
|
|
797
841
|
import { promisify } from "util";
|
|
798
842
|
import { fileURLToPath } from "url";
|
|
799
843
|
|
|
800
|
-
// src/core/
|
|
801
|
-
import
|
|
802
|
-
|
|
844
|
+
// src/core/ast-editing/editors/code-editor-factory.ts
|
|
845
|
+
import * as path4 from "path";
|
|
846
|
+
|
|
847
|
+
// src/core/ast-editing/editors/typescript-editor.ts
|
|
848
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
849
|
+
import * as path3 from "path";
|
|
850
|
+
import * as fs3 from "fs";
|
|
851
|
+
var TypeScriptEditor = class {
|
|
852
|
+
project;
|
|
853
|
+
constructor() {
|
|
854
|
+
this.project = new Project({
|
|
855
|
+
skipAddingFilesFromTsConfig: true,
|
|
856
|
+
compilerOptions: {
|
|
857
|
+
target: 99,
|
|
858
|
+
// ESNext
|
|
859
|
+
module: 99
|
|
860
|
+
// ESNext
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
803
864
|
/**
|
|
804
|
-
*
|
|
805
|
-
* This string is intended to be appended to the user Preview so the Agent can see it.
|
|
865
|
+
* Get or add source file to project
|
|
806
866
|
*/
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
867
|
+
getSourceFile(filePath) {
|
|
868
|
+
const absolutePath = path3.resolve(filePath);
|
|
869
|
+
let sourceFile = this.project.getSourceFile(absolutePath);
|
|
870
|
+
if (!sourceFile) {
|
|
871
|
+
if (!fs3.existsSync(absolutePath)) {
|
|
872
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
873
|
+
}
|
|
874
|
+
sourceFile = this.project.addSourceFileAtPath(absolutePath);
|
|
875
|
+
}
|
|
876
|
+
return sourceFile;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Get class declaration by name
|
|
880
|
+
*/
|
|
881
|
+
getClass(sourceFile, className) {
|
|
882
|
+
const classDecl = sourceFile.getClass(className);
|
|
883
|
+
if (!classDecl) {
|
|
884
|
+
throw new Error(`Class "${className}" not found in ${sourceFile.getFilePath()}`);
|
|
885
|
+
}
|
|
886
|
+
return classDecl;
|
|
887
|
+
}
|
|
888
|
+
// ═══════════════════════════════════════════════════════
|
|
889
|
+
// IMPLEMENTATION: listStructure
|
|
890
|
+
// ═══════════════════════════════════════════════════════
|
|
891
|
+
async listStructure(filePath) {
|
|
892
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
893
|
+
const classes = sourceFile.getClasses().map((cls) => ({
|
|
894
|
+
name: cls.getName() || "<anonymous>",
|
|
895
|
+
methods: cls.getMethods().map((m) => this.extractMethodInfo(m)),
|
|
896
|
+
properties: cls.getProperties().map((p) => this.extractPropertyInfo(p)),
|
|
897
|
+
decorators: cls.getDecorators().map((d) => d.getText()),
|
|
898
|
+
extendsClass: cls.getExtends()?.getText(),
|
|
899
|
+
implementsInterfaces: cls.getImplements().map((i) => i.getText())
|
|
900
|
+
}));
|
|
901
|
+
const interfaces = sourceFile.getInterfaces().map((iface) => ({
|
|
902
|
+
name: iface.getName(),
|
|
903
|
+
properties: iface.getProperties().map((p) => this.extractPropertyInfo(p)),
|
|
904
|
+
extends: iface.getExtends().map((e) => e.getText())
|
|
905
|
+
}));
|
|
906
|
+
const functions = sourceFile.getFunctions().map((fn) => ({
|
|
907
|
+
name: fn.getName() || "<anonymous>",
|
|
908
|
+
parameters: fn.getParameters().map((p) => ({
|
|
909
|
+
name: p.getName(),
|
|
910
|
+
type: p.getType().getText(),
|
|
911
|
+
isOptional: p.isOptional()
|
|
912
|
+
})),
|
|
913
|
+
returnType: fn.getReturnType().getText(),
|
|
914
|
+
isAsync: fn.isAsync(),
|
|
915
|
+
isExported: fn.isExported()
|
|
916
|
+
}));
|
|
917
|
+
const imports = sourceFile.getImportDeclarations().map((imp) => ({
|
|
918
|
+
modulePath: imp.getModuleSpecifierValue(),
|
|
919
|
+
isDefault: !!imp.getDefaultImport(),
|
|
920
|
+
namedImports: imp.getNamedImports().map((n) => n.getName()),
|
|
921
|
+
namespaceImport: imp.getNamespaceImport()?.getText()
|
|
922
|
+
}));
|
|
923
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
924
|
+
const exportInfo = Array.from(exports.entries()).flatMap(
|
|
925
|
+
([name, declarations]) => declarations.map((decl) => ({
|
|
926
|
+
name,
|
|
927
|
+
type: this.getDeclarationType(decl),
|
|
928
|
+
isDefault: sourceFile.getDefaultExportSymbol()?.getName() === name
|
|
929
|
+
}))
|
|
930
|
+
);
|
|
931
|
+
return {
|
|
932
|
+
classes,
|
|
933
|
+
interfaces,
|
|
934
|
+
functions,
|
|
935
|
+
imports,
|
|
936
|
+
exports: exportInfo
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
extractMethodInfo(method) {
|
|
940
|
+
return {
|
|
941
|
+
name: method.getName(),
|
|
942
|
+
parameters: method.getParameters().map((p) => ({
|
|
943
|
+
name: p.getName(),
|
|
944
|
+
type: p.getType().getText(),
|
|
945
|
+
isOptional: p.isOptional()
|
|
946
|
+
})),
|
|
947
|
+
returnType: method.getReturnType().getText(),
|
|
948
|
+
isAsync: method.isAsync(),
|
|
949
|
+
isStatic: method.isStatic(),
|
|
950
|
+
visibility: this.getVisibility(method),
|
|
951
|
+
decorators: method.getDecorators().map((d) => d.getText())
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
extractPropertyInfo(property) {
|
|
955
|
+
return {
|
|
956
|
+
name: property.getName(),
|
|
957
|
+
type: property.getType()?.getText(),
|
|
958
|
+
visibility: this.getVisibility(property),
|
|
959
|
+
isReadonly: property.isReadonly(),
|
|
960
|
+
initializer: property.getInitializer()?.getText()
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
getVisibility(node) {
|
|
964
|
+
if (node.hasModifier?.(SyntaxKind.PrivateKeyword)) return "private";
|
|
965
|
+
if (node.hasModifier?.(SyntaxKind.ProtectedKeyword)) return "protected";
|
|
966
|
+
return "public";
|
|
967
|
+
}
|
|
968
|
+
getDeclarationType(decl) {
|
|
969
|
+
if (decl.getKind() === SyntaxKind.ClassDeclaration) return "class";
|
|
970
|
+
if (decl.getKind() === SyntaxKind.FunctionDeclaration) return "function";
|
|
971
|
+
if (decl.getKind() === SyntaxKind.InterfaceDeclaration) return "interface";
|
|
972
|
+
if (decl.getKind() === SyntaxKind.TypeAliasDeclaration) return "type";
|
|
973
|
+
return "const";
|
|
974
|
+
}
|
|
975
|
+
// ═══════════════════════════════════════════════════════
|
|
976
|
+
// IMPLEMENTATION: Class Operations
|
|
977
|
+
// ═══════════════════════════════════════════════════════
|
|
978
|
+
async addClass(filePath, className, options) {
|
|
979
|
+
try {
|
|
980
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
981
|
+
sourceFile.addClass({
|
|
982
|
+
name: className,
|
|
983
|
+
extends: options?.extendsClass,
|
|
984
|
+
implements: options?.implementsInterfaces,
|
|
985
|
+
isExported: true
|
|
986
|
+
});
|
|
987
|
+
await sourceFile.save();
|
|
988
|
+
return true;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
console.error(`Failed to add class "${className}":`, error);
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async addProperty(filePath, className, propertyCode) {
|
|
995
|
+
try {
|
|
996
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
997
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
998
|
+
const closeBrace = classDecl.getEnd() - 1;
|
|
999
|
+
classDecl.insertText(closeBrace, `
|
|
1000
|
+
${propertyCode}`);
|
|
1001
|
+
classDecl.formatText();
|
|
1002
|
+
await sourceFile.save();
|
|
1003
|
+
return true;
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
console.error(`Failed to add property to "${className}":`, error);
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async removeProperty(filePath, className, propertyName) {
|
|
1010
|
+
try {
|
|
1011
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1012
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1013
|
+
const property = classDecl.getProperty(propertyName);
|
|
1014
|
+
if (!property) {
|
|
1015
|
+
throw new Error(`Property "${propertyName}" not found in class "${className}"`);
|
|
1016
|
+
}
|
|
1017
|
+
property.remove();
|
|
1018
|
+
await sourceFile.save();
|
|
1019
|
+
return true;
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error(`Failed to remove property "${propertyName}":`, error);
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async addMethod(filePath, className, methodCode) {
|
|
1026
|
+
try {
|
|
1027
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1028
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1029
|
+
const closeBrace = classDecl.getEnd() - 1;
|
|
1030
|
+
classDecl.insertText(closeBrace, `
|
|
1031
|
+
${methodCode}
|
|
1032
|
+
`);
|
|
1033
|
+
classDecl.formatText();
|
|
1034
|
+
await sourceFile.save();
|
|
1035
|
+
return true;
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
console.error(`Failed to add method to "${className}":`, error);
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async modifyMethod(filePath, className, methodName, newBody) {
|
|
1042
|
+
try {
|
|
1043
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1044
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1045
|
+
const method = classDecl.getMethod(methodName);
|
|
1046
|
+
if (!method) {
|
|
1047
|
+
throw new Error(`Method "${methodName}" not found in class "${className}"`);
|
|
1048
|
+
}
|
|
1049
|
+
method.setBodyText(newBody);
|
|
1050
|
+
method.formatText();
|
|
1051
|
+
await sourceFile.save();
|
|
1052
|
+
return true;
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.error(`Failed to modify method "${methodName}":`, error);
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async removeMethod(filePath, className, methodName) {
|
|
1059
|
+
try {
|
|
1060
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1061
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1062
|
+
const method = classDecl.getMethod(methodName);
|
|
1063
|
+
if (!method) {
|
|
1064
|
+
throw new Error(`Method "${methodName}" not found in class "${className}"`);
|
|
1065
|
+
}
|
|
1066
|
+
method.remove();
|
|
1067
|
+
await sourceFile.save();
|
|
1068
|
+
return true;
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
console.error(`Failed to remove method "${methodName}":`, error);
|
|
1071
|
+
return false;
|
|
831
1072
|
}
|
|
1073
|
+
}
|
|
1074
|
+
async addDecorator(filePath, className, decoratorCode) {
|
|
832
1075
|
try {
|
|
833
|
-
const
|
|
834
|
-
const
|
|
835
|
-
const
|
|
1076
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1077
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1078
|
+
const start = classDecl.getStart();
|
|
1079
|
+
sourceFile.insertText(start, `${decoratorCode}
|
|
1080
|
+
`);
|
|
1081
|
+
sourceFile.formatText();
|
|
1082
|
+
await sourceFile.save();
|
|
1083
|
+
return true;
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error(`Failed to add decorator to "${className}":`, error);
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async getMethod(filePath, className, methodName) {
|
|
1090
|
+
try {
|
|
1091
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1092
|
+
const classDecl = this.getClass(sourceFile, className);
|
|
1093
|
+
const method = classDecl.getMethod(methodName);
|
|
1094
|
+
if (!method) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
return method.getText();
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
console.error(`Failed to get method "${methodName}":`, error);
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// ═══════════════════════════════════════════════════════
|
|
1104
|
+
// IMPLEMENTATION: Interface/Type Operations
|
|
1105
|
+
// ═══════════════════════════════════════════════════════
|
|
1106
|
+
async addInterface(filePath, interfaceCode) {
|
|
1107
|
+
try {
|
|
1108
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1109
|
+
sourceFile.insertText(sourceFile.getEnd(), `
|
|
836
1110
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1111
|
+
${interfaceCode}`);
|
|
1112
|
+
sourceFile.formatText();
|
|
1113
|
+
await sourceFile.save();
|
|
1114
|
+
return true;
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
console.error(`Failed to add interface:`, error);
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async addTypeAlias(filePath, typeCode) {
|
|
1121
|
+
try {
|
|
1122
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1123
|
+
sourceFile.insertText(sourceFile.getEnd(), `
|
|
841
1124
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
rawText: cleanedText
|
|
875
|
-
});
|
|
876
|
-
return `\u{1F916} **[AI Code Review Agent]**
|
|
877
|
-
\u26A0\uFE0F Response format error. Raw output:
|
|
878
|
-
${cleanedText}`;
|
|
1125
|
+
${typeCode}`);
|
|
1126
|
+
sourceFile.formatText();
|
|
1127
|
+
await sourceFile.save();
|
|
1128
|
+
return true;
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.error(`Failed to add type alias:`, error);
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// ═══════════════════════════════════════════════════════
|
|
1135
|
+
// IMPLEMENTATION: Function Operations
|
|
1136
|
+
// ═══════════════════════════════════════════════════════
|
|
1137
|
+
async addFunction(filePath, functionCode) {
|
|
1138
|
+
try {
|
|
1139
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1140
|
+
sourceFile.insertText(sourceFile.getEnd(), `
|
|
1141
|
+
|
|
1142
|
+
${functionCode}`);
|
|
1143
|
+
sourceFile.formatText();
|
|
1144
|
+
await sourceFile.save();
|
|
1145
|
+
return true;
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
console.error(`Failed to add function:`, error);
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async removeFunction(filePath, functionName) {
|
|
1152
|
+
try {
|
|
1153
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1154
|
+
const func = sourceFile.getFunction(functionName);
|
|
1155
|
+
if (!func) {
|
|
1156
|
+
throw new Error(`Function "${functionName}" not found`);
|
|
879
1157
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
return
|
|
883
|
-
|
|
1158
|
+
func.remove();
|
|
1159
|
+
await sourceFile.save();
|
|
1160
|
+
return true;
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
console.error(`Failed to remove function "${functionName}":`, error);
|
|
1163
|
+
return false;
|
|
884
1164
|
}
|
|
885
1165
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1166
|
+
// ═══════════════════════════════════════════════════════
|
|
1167
|
+
// IMPLEMENTATION: Import/Export Operations
|
|
1168
|
+
// ═══════════════════════════════════════════════════════
|
|
1169
|
+
async addImport(filePath, importStatement) {
|
|
1170
|
+
try {
|
|
1171
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1172
|
+
const lastImport = sourceFile.getImportDeclarations().pop();
|
|
1173
|
+
const pos = lastImport ? lastImport.getEnd() : 0;
|
|
1174
|
+
sourceFile.insertText(pos, `
|
|
1175
|
+
${importStatement}`);
|
|
1176
|
+
this.organizeImports(filePath);
|
|
1177
|
+
await sourceFile.save();
|
|
1178
|
+
return true;
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
console.error(`Failed to add import:`, error);
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async removeImport(filePath, modulePath) {
|
|
1185
|
+
try {
|
|
1186
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1187
|
+
const importDecls = sourceFile.getImportDeclarations().filter((imp) => imp.getModuleSpecifierValue() === modulePath);
|
|
1188
|
+
if (importDecls.length === 0) {
|
|
1189
|
+
throw new Error(`Import from "${modulePath}" not found`);
|
|
905
1190
|
}
|
|
1191
|
+
importDecls.forEach((d) => d.remove());
|
|
1192
|
+
await sourceFile.save();
|
|
1193
|
+
return true;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
console.error(`Failed to remove import from "${modulePath}":`, error);
|
|
1196
|
+
return false;
|
|
906
1197
|
}
|
|
907
|
-
report += `
|
|
908
|
-
**Resumo:** ${data.summary}`;
|
|
909
|
-
return report;
|
|
910
1198
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1199
|
+
async organizeImports(filePath) {
|
|
1200
|
+
try {
|
|
1201
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
1202
|
+
sourceFile.organizeImports();
|
|
1203
|
+
const imports = sourceFile.getImportDeclarations();
|
|
1204
|
+
const importStructure = imports.map((i) => i.getStructure());
|
|
1205
|
+
importStructure.sort((a, b) => {
|
|
1206
|
+
return a.moduleSpecifier.localeCompare(b.moduleSpecifier);
|
|
1207
|
+
});
|
|
1208
|
+
imports.forEach((i) => i.remove());
|
|
1209
|
+
sourceFile.addImportDeclarations(importStructure);
|
|
1210
|
+
await sourceFile.save();
|
|
1211
|
+
return true;
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
console.error(`Failed to organize imports:`, error);
|
|
1214
|
+
return false;
|
|
923
1215
|
}
|
|
924
|
-
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// src/core/ast-editing/editors/code-editor-factory.ts
|
|
1220
|
+
var CodeEditorFactory = class {
|
|
1221
|
+
static tsEditor = null;
|
|
1222
|
+
/**
|
|
1223
|
+
* Returns appropriate editor for the file, or null if AST editing not supported
|
|
1224
|
+
*/
|
|
1225
|
+
static getEditor(filePath) {
|
|
1226
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
1227
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
1228
|
+
if (!this.tsEditor) {
|
|
1229
|
+
this.tsEditor = new TypeScriptEditor();
|
|
1230
|
+
}
|
|
1231
|
+
return this.tsEditor;
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Clear cached editors (useful for testing)
|
|
1237
|
+
*/
|
|
1238
|
+
static clearCache() {
|
|
1239
|
+
this.tsEditor = null;
|
|
925
1240
|
}
|
|
926
1241
|
};
|
|
927
1242
|
|
|
@@ -934,9 +1249,9 @@ function detectLineEnding(content) {
|
|
|
934
1249
|
}
|
|
935
1250
|
function handleListFiles(dirPath) {
|
|
936
1251
|
try {
|
|
937
|
-
const fullPath =
|
|
938
|
-
if (!
|
|
939
|
-
const items =
|
|
1252
|
+
const fullPath = path5.resolve(process.cwd(), dirPath);
|
|
1253
|
+
if (!fs4.existsSync(fullPath)) return `Error: Directory ${dirPath} does not exist.`;
|
|
1254
|
+
const items = fs4.readdirSync(fullPath, { withFileTypes: true });
|
|
940
1255
|
return items.map((item) => {
|
|
941
1256
|
return `${item.isDirectory() ? "[DIR]" : "[FILE]"} ${item.name}`;
|
|
942
1257
|
}).join("\n");
|
|
@@ -946,11 +1261,11 @@ function handleListFiles(dirPath) {
|
|
|
946
1261
|
}
|
|
947
1262
|
function handleReadFile(filePath, showLineNumbers = true) {
|
|
948
1263
|
try {
|
|
949
|
-
const fullPath =
|
|
950
|
-
if (!
|
|
951
|
-
const stats =
|
|
1264
|
+
const fullPath = path5.resolve(process.cwd(), filePath);
|
|
1265
|
+
if (!fs4.existsSync(fullPath)) return `Error: File ${filePath} does not exist.`;
|
|
1266
|
+
const stats = fs4.statSync(fullPath);
|
|
952
1267
|
if (stats.size > 100 * 1024) return `Error: File too large to read (${stats.size} bytes). Limit is 100KB.`;
|
|
953
|
-
const content =
|
|
1268
|
+
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
954
1269
|
if (showLineNumbers) {
|
|
955
1270
|
const lines = content.split("\n");
|
|
956
1271
|
return lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
@@ -962,11 +1277,11 @@ function handleReadFile(filePath, showLineNumbers = true) {
|
|
|
962
1277
|
}
|
|
963
1278
|
function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
|
|
964
1279
|
try {
|
|
965
|
-
if (!
|
|
1280
|
+
if (!fs4.existsSync(filePath)) {
|
|
966
1281
|
tui2.log.error(`\u274C File not found for modification: ${filePath}`);
|
|
967
1282
|
return false;
|
|
968
1283
|
}
|
|
969
|
-
const currentFileContent =
|
|
1284
|
+
const currentFileContent = fs4.readFileSync(filePath, "utf-8");
|
|
970
1285
|
const lineEnding = detectLineEnding(currentFileContent);
|
|
971
1286
|
const lines = currentFileContent.split(lineEnding);
|
|
972
1287
|
if (startLine < 1 || startLine > lines.length) {
|
|
@@ -984,7 +1299,7 @@ function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
|
|
|
984
1299
|
const result = [...before, ...normalizedNewLines, ...after].join(lineEnding);
|
|
985
1300
|
const BOM = "\uFEFF";
|
|
986
1301
|
const finalContent = result.startsWith(BOM) ? result : BOM + result;
|
|
987
|
-
|
|
1302
|
+
fs4.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
|
|
988
1303
|
tui2.log.success(`\u2705 Replaced lines ${startLine}-${endLine} in ${filePath}`);
|
|
989
1304
|
return true;
|
|
990
1305
|
} catch (e) {
|
|
@@ -992,47 +1307,6 @@ function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
|
|
|
992
1307
|
return false;
|
|
993
1308
|
}
|
|
994
1309
|
}
|
|
995
|
-
async function generateFilePreview(filePath, startLine, endLine, newContent) {
|
|
996
|
-
try {
|
|
997
|
-
const fullPath = path4.resolve(process.cwd(), filePath);
|
|
998
|
-
if (!fs3.existsSync(fullPath)) return `Error: File ${filePath} does not exist.`;
|
|
999
|
-
const content = fs3.readFileSync(fullPath, "utf-8");
|
|
1000
|
-
const lines = content.split(/\r\n|\r|\n/);
|
|
1001
|
-
const startIdx = startLine - 1;
|
|
1002
|
-
const endIdx = endLine - 1;
|
|
1003
|
-
if (startIdx < 0 || startIdx >= lines.length) return `Error: Start line ${startLine} is out of bounds (File has ${lines.length} lines).`;
|
|
1004
|
-
if (endIdx < startIdx || endIdx >= lines.length) return `Error: End line ${endLine} is invalid.`;
|
|
1005
|
-
const contextBefore = lines.slice(Math.max(0, startIdx - 3), startIdx).map((l, i) => `${Math.max(1, startLine - 3) + i} | ${l}`).join("\n");
|
|
1006
|
-
const contentReplacing = lines.slice(startIdx, endIdx + 1).map((l, i) => `${startLine + i} | - ${l}`).join("\n");
|
|
1007
|
-
const contextAfter = lines.slice(endIdx + 1, Math.min(lines.length, endIdx + 4)).map((l, i) => `${endLine + 1 + i} | ${l}`).join("\n");
|
|
1008
|
-
const newLines = newContent.split("\n").map((l) => `+ ${l}`).join("\n");
|
|
1009
|
-
let reviewFeedback = "";
|
|
1010
|
-
try {
|
|
1011
|
-
reviewFeedback = await CodeReviewService.reviewCode(filePath, newContent);
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
reviewFeedback = `\u26A0\uFE0F Code Review Service Unavailable: ${err.message}`;
|
|
1014
|
-
}
|
|
1015
|
-
return `PREVIEW OF CHANGES to ${filePath}:
|
|
1016
|
-
--------------------------------------------------
|
|
1017
|
-
CONTEXT BEFORE:
|
|
1018
|
-
${contextBefore}
|
|
1019
|
-
|
|
1020
|
-
CHANGES:
|
|
1021
|
-
${contentReplacing}
|
|
1022
|
-
${newLines}
|
|
1023
|
-
|
|
1024
|
-
CONTEXT AFTER:
|
|
1025
|
-
${contextAfter}
|
|
1026
|
-
--------------------------------------------------
|
|
1027
|
-
${reviewFeedback}
|
|
1028
|
-
|
|
1029
|
-
IMPORTANT: Please verify that the lines being replaced (marked with -) are exactly what you intend to remove.
|
|
1030
|
-
If the context looks wrong, DO NOT CONFIRM. Re-read the file to check line numbers.
|
|
1031
|
-
`;
|
|
1032
|
-
} catch (e) {
|
|
1033
|
-
return `Error generating preview: ${e.message}`;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
1310
|
function handleSearchFile(pattern) {
|
|
1037
1311
|
try {
|
|
1038
1312
|
const entries = fg.sync(pattern, { dot: true });
|
|
@@ -1043,11 +1317,11 @@ function handleSearchFile(pattern) {
|
|
|
1043
1317
|
}
|
|
1044
1318
|
}
|
|
1045
1319
|
function startSmartReplace(filePath, newContent, targetContent, tui2) {
|
|
1046
|
-
if (!
|
|
1320
|
+
if (!fs4.existsSync(filePath)) {
|
|
1047
1321
|
tui2.log.error(`\u274C File not found for modification: ${filePath}`);
|
|
1048
1322
|
return false;
|
|
1049
1323
|
}
|
|
1050
|
-
const currentFileContent =
|
|
1324
|
+
const currentFileContent = fs4.readFileSync(filePath, "utf-8");
|
|
1051
1325
|
const normalizedTarget = targetContent.replace(/\r\n/g, "\n");
|
|
1052
1326
|
const normalizedContent = currentFileContent.replace(/\r\n/g, "\n");
|
|
1053
1327
|
if (!normalizedContent.includes(normalizedTarget)) {
|
|
@@ -1064,7 +1338,7 @@ function startSmartReplace(filePath, newContent, targetContent, tui2) {
|
|
|
1064
1338
|
const BOM = "\uFEFF";
|
|
1065
1339
|
const updatedContent = currentFileContent.replace(targetContent, newContent);
|
|
1066
1340
|
const finalContent = updatedContent.startsWith(BOM) ? updatedContent : BOM + updatedContent;
|
|
1067
|
-
|
|
1341
|
+
fs4.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
|
|
1068
1342
|
tui2.log.success(`\u2705 Smart Replace Applied: ${filePath}`);
|
|
1069
1343
|
return true;
|
|
1070
1344
|
}
|
|
@@ -1072,7 +1346,7 @@ async function handleRunCommand(command) {
|
|
|
1072
1346
|
const { spawn } = await import("child_process");
|
|
1073
1347
|
try {
|
|
1074
1348
|
tui.log.info(`\u{1F4BB} Executing: ${colors.dim(command)}`);
|
|
1075
|
-
return new Promise((
|
|
1349
|
+
return new Promise((resolve2) => {
|
|
1076
1350
|
const child = spawn(command, {
|
|
1077
1351
|
shell: true,
|
|
1078
1352
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1082,7 +1356,7 @@ async function handleRunCommand(command) {
|
|
|
1082
1356
|
let stderr = "";
|
|
1083
1357
|
const timer = setTimeout(() => {
|
|
1084
1358
|
child.kill();
|
|
1085
|
-
|
|
1359
|
+
resolve2(`Error: Command timed out after 5 minutes.
|
|
1086
1360
|
Output so far:
|
|
1087
1361
|
${stdout}
|
|
1088
1362
|
${stderr}`);
|
|
@@ -1097,9 +1371,9 @@ ${stderr}`);
|
|
|
1097
1371
|
child.on("close", (code) => {
|
|
1098
1372
|
clearTimeout(timer);
|
|
1099
1373
|
if (code === 0) {
|
|
1100
|
-
|
|
1374
|
+
resolve2(stdout.trim() || "Command executed successfully (no output).");
|
|
1101
1375
|
} else {
|
|
1102
|
-
|
|
1376
|
+
resolve2(`Command failed with exit code ${code}.
|
|
1103
1377
|
STDERR:
|
|
1104
1378
|
${stderr}
|
|
1105
1379
|
STDOUT:
|
|
@@ -1108,7 +1382,7 @@ ${stdout}`);
|
|
|
1108
1382
|
});
|
|
1109
1383
|
child.on("error", (err) => {
|
|
1110
1384
|
clearTimeout(timer);
|
|
1111
|
-
|
|
1385
|
+
resolve2(`Error executing command: ${err.message}`);
|
|
1112
1386
|
});
|
|
1113
1387
|
});
|
|
1114
1388
|
} catch (e) {
|
|
@@ -1120,20 +1394,20 @@ function resolveAstGrepCommand() {
|
|
|
1120
1394
|
const binName = isWin ? "sg.cmd" : "sg";
|
|
1121
1395
|
try {
|
|
1122
1396
|
const currentFile = fileURLToPath(import.meta.url);
|
|
1123
|
-
let dir =
|
|
1397
|
+
let dir = path5.dirname(currentFile);
|
|
1124
1398
|
for (let i = 0; i < 5; i++) {
|
|
1125
|
-
const candidate =
|
|
1126
|
-
if (
|
|
1399
|
+
const candidate = path5.join(dir, "node_modules", ".bin", binName);
|
|
1400
|
+
if (fs4.existsSync(candidate)) {
|
|
1127
1401
|
return `"${candidate}"`;
|
|
1128
1402
|
}
|
|
1129
|
-
const parent =
|
|
1403
|
+
const parent = path5.dirname(dir);
|
|
1130
1404
|
if (parent === dir) break;
|
|
1131
1405
|
dir = parent;
|
|
1132
1406
|
}
|
|
1133
1407
|
} catch (e) {
|
|
1134
1408
|
}
|
|
1135
|
-
const cwdBin =
|
|
1136
|
-
if (
|
|
1409
|
+
const cwdBin = path5.resolve(process.cwd(), "node_modules", ".bin", binName);
|
|
1410
|
+
if (fs4.existsSync(cwdBin)) {
|
|
1137
1411
|
return `"${cwdBin}"`;
|
|
1138
1412
|
}
|
|
1139
1413
|
return "npx sg";
|
|
@@ -1141,13 +1415,13 @@ function resolveAstGrepCommand() {
|
|
|
1141
1415
|
async function astGrepSearch(pattern, filePath, language, tui2) {
|
|
1142
1416
|
const { spawn } = await import("child_process");
|
|
1143
1417
|
try {
|
|
1144
|
-
if (!
|
|
1418
|
+
if (!fs4.existsSync(filePath)) {
|
|
1145
1419
|
return `\u274C File not found: ${filePath}`;
|
|
1146
1420
|
}
|
|
1147
1421
|
const sgCmd = resolveAstGrepCommand();
|
|
1148
1422
|
const cmd = `${sgCmd} run -p "${pattern}" -l ${language} --json ${filePath}`;
|
|
1149
1423
|
tui2.log.info(`\u{1F50D} [AST-GREP] Searching: ${cmd}`);
|
|
1150
|
-
return new Promise((
|
|
1424
|
+
return new Promise((resolve2) => {
|
|
1151
1425
|
const child = spawn(cmd, {
|
|
1152
1426
|
shell: true,
|
|
1153
1427
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1161,19 +1435,19 @@ async function astGrepSearch(pattern, filePath, language, tui2) {
|
|
|
1161
1435
|
child.stderr.on("data", (data) => stderr += data.toString());
|
|
1162
1436
|
child.on("close", (code) => {
|
|
1163
1437
|
if (code === 0 && stdout) {
|
|
1164
|
-
|
|
1438
|
+
resolve2(stdout);
|
|
1165
1439
|
} else if (code === 1 && !stderr) {
|
|
1166
|
-
|
|
1440
|
+
resolve2("No structural matches found.");
|
|
1167
1441
|
} else {
|
|
1168
|
-
if (!stdout && !stderr)
|
|
1442
|
+
if (!stdout && !stderr) resolve2("No structural matches found.");
|
|
1169
1443
|
else {
|
|
1170
1444
|
tui2.log.error(`\u274C ast-grep search error (code ${code}): ${stderr}`);
|
|
1171
|
-
|
|
1445
|
+
resolve2(`Error executing ast-grep search: ${stderr || stdout}`);
|
|
1172
1446
|
}
|
|
1173
1447
|
}
|
|
1174
1448
|
});
|
|
1175
1449
|
child.on("error", (err) => {
|
|
1176
|
-
|
|
1450
|
+
resolve2(`Error executing ast-grep search: ${err.message}`);
|
|
1177
1451
|
});
|
|
1178
1452
|
});
|
|
1179
1453
|
} catch (e) {
|
|
@@ -1184,14 +1458,14 @@ async function astGrepSearch(pattern, filePath, language, tui2) {
|
|
|
1184
1458
|
async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
|
|
1185
1459
|
const { spawn } = await import("child_process");
|
|
1186
1460
|
try {
|
|
1187
|
-
if (!
|
|
1461
|
+
if (!fs4.existsSync(filePath)) {
|
|
1188
1462
|
tui2.log.error(`\u274C File not found for AST modification: ${filePath}`);
|
|
1189
1463
|
return false;
|
|
1190
1464
|
}
|
|
1191
1465
|
const sgCmd = resolveAstGrepCommand();
|
|
1192
1466
|
const cmd = `${sgCmd} run -p "${pattern}" -r "${fix}" -l ${language} ${filePath} --update-all`;
|
|
1193
1467
|
tui2.log.info(`\u270F\uFE0F [AST-GREP] Rewriting: pattern="${pattern}" fix="${fix.substring(0, 50)}..."`);
|
|
1194
|
-
return new Promise((
|
|
1468
|
+
return new Promise((resolve2) => {
|
|
1195
1469
|
const child = spawn(cmd, {
|
|
1196
1470
|
shell: true,
|
|
1197
1471
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1202,15 +1476,15 @@ async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
|
|
|
1202
1476
|
child.on("close", (code) => {
|
|
1203
1477
|
if (code === 0) {
|
|
1204
1478
|
tui2.log.success(`\u2705 AST Rewrite applied to ${filePath}`);
|
|
1205
|
-
|
|
1479
|
+
resolve2(true);
|
|
1206
1480
|
} else {
|
|
1207
1481
|
tui2.log.error(`\u274C AST Rewrite failed (code ${code}): ${stderr}`);
|
|
1208
|
-
|
|
1482
|
+
resolve2(false);
|
|
1209
1483
|
}
|
|
1210
1484
|
});
|
|
1211
1485
|
child.on("error", (err) => {
|
|
1212
1486
|
tui2.log.error(`\u274C AST Rewrite spawn error: ${err.message}`);
|
|
1213
|
-
|
|
1487
|
+
resolve2(false);
|
|
1214
1488
|
});
|
|
1215
1489
|
});
|
|
1216
1490
|
} catch (e) {
|
|
@@ -1218,6 +1492,173 @@ async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
|
|
|
1218
1492
|
return false;
|
|
1219
1493
|
}
|
|
1220
1494
|
}
|
|
1495
|
+
async function astListStructure(filePath) {
|
|
1496
|
+
try {
|
|
1497
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1498
|
+
if (!editor) {
|
|
1499
|
+
return `[AST Error] File type not supported: ${filePath}. Use read_file instead.`;
|
|
1500
|
+
}
|
|
1501
|
+
const structure = await editor.listStructure(filePath);
|
|
1502
|
+
let output = `[AST Structure of ${filePath}]
|
|
1503
|
+
|
|
1504
|
+
`;
|
|
1505
|
+
if (structure.classes.length > 0) {
|
|
1506
|
+
output += `CLASSES:
|
|
1507
|
+
`;
|
|
1508
|
+
structure.classes.forEach((cls) => {
|
|
1509
|
+
output += ` - ${cls.name}`;
|
|
1510
|
+
if (cls.extendsClass) output += ` extends ${cls.extendsClass}`;
|
|
1511
|
+
if (cls.implementsInterfaces.length > 0) {
|
|
1512
|
+
output += ` implements ${cls.implementsInterfaces.join(", ")}`;
|
|
1513
|
+
}
|
|
1514
|
+
output += `
|
|
1515
|
+
`;
|
|
1516
|
+
if (cls.decorators.length > 0) {
|
|
1517
|
+
output += ` Decorators: ${cls.decorators.join(", ")}
|
|
1518
|
+
`;
|
|
1519
|
+
}
|
|
1520
|
+
if (cls.properties.length > 0) {
|
|
1521
|
+
output += ` Properties:
|
|
1522
|
+
`;
|
|
1523
|
+
cls.properties.forEach((prop) => {
|
|
1524
|
+
output += ` - ${prop.visibility} ${prop.name}: ${prop.type}
|
|
1525
|
+
`;
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
if (cls.methods.length > 0) {
|
|
1529
|
+
output += ` Methods:
|
|
1530
|
+
`;
|
|
1531
|
+
cls.methods.forEach((method) => {
|
|
1532
|
+
const params = method.parameters.map((p) => `${p.name}: ${p.type}`).join(", ");
|
|
1533
|
+
output += ` - ${method.visibility} ${method.name}(${params}): ${method.returnType}
|
|
1534
|
+
`;
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
output += `
|
|
1538
|
+
`;
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
if (structure.interfaces.length > 0) {
|
|
1542
|
+
output += `INTERFACES:
|
|
1543
|
+
`;
|
|
1544
|
+
structure.interfaces.forEach((iface) => {
|
|
1545
|
+
output += ` - ${iface.name}
|
|
1546
|
+
`;
|
|
1547
|
+
iface.properties.forEach((prop) => {
|
|
1548
|
+
output += ` ${prop.name}: ${prop.type}
|
|
1549
|
+
`;
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
1552
|
+
output += `
|
|
1553
|
+
`;
|
|
1554
|
+
}
|
|
1555
|
+
if (structure.functions.length > 0) {
|
|
1556
|
+
output += `FUNCTIONS:
|
|
1557
|
+
`;
|
|
1558
|
+
structure.functions.forEach((fn) => {
|
|
1559
|
+
const params = fn.parameters.map((p) => `${p.name}: ${p.type}`).join(", ");
|
|
1560
|
+
output += ` - ${fn.name}(${params}): ${fn.returnType}
|
|
1561
|
+
`;
|
|
1562
|
+
});
|
|
1563
|
+
output += `
|
|
1564
|
+
`;
|
|
1565
|
+
}
|
|
1566
|
+
if (structure.imports.length > 0) {
|
|
1567
|
+
output += `IMPORTS:
|
|
1568
|
+
`;
|
|
1569
|
+
structure.imports.forEach((imp) => {
|
|
1570
|
+
output += ` - from "${imp.modulePath}": ${imp.namedImports.join(", ")}
|
|
1571
|
+
`;
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
return output;
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
return `[AST Error] ${error.message}`;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function astAddMethod(filePath, className, methodCode) {
|
|
1580
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1581
|
+
if (!editor) {
|
|
1582
|
+
throw new Error(`No AST editor available for ${filePath}`);
|
|
1583
|
+
}
|
|
1584
|
+
return await editor.addMethod(filePath, className, methodCode);
|
|
1585
|
+
}
|
|
1586
|
+
async function astGetMethod(filePath, className, methodName) {
|
|
1587
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1588
|
+
if (!editor) {
|
|
1589
|
+
throw new Error(`No AST editor available for ${filePath}`);
|
|
1590
|
+
}
|
|
1591
|
+
const methodContent = await editor.getMethod(filePath, className, methodName);
|
|
1592
|
+
if (!methodContent) {
|
|
1593
|
+
return `Method "${methodName}" not found in class "${className}"`;
|
|
1594
|
+
}
|
|
1595
|
+
return methodContent;
|
|
1596
|
+
}
|
|
1597
|
+
async function astAddClass(filePath, className, extendsClass, implementsInterfaces) {
|
|
1598
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1599
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1600
|
+
return await editor.addClass(filePath, className, { extendsClass, implementsInterfaces });
|
|
1601
|
+
}
|
|
1602
|
+
async function astAddProperty(filePath, className, propertyCode) {
|
|
1603
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1604
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1605
|
+
return await editor.addProperty(filePath, className, propertyCode);
|
|
1606
|
+
}
|
|
1607
|
+
async function astRemoveProperty(filePath, className, propertyName) {
|
|
1608
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1609
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1610
|
+
return await editor.removeProperty(filePath, className, propertyName);
|
|
1611
|
+
}
|
|
1612
|
+
async function astModifyMethod(filePath, className, methodName, newBody) {
|
|
1613
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1614
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1615
|
+
return await editor.modifyMethod(filePath, className, methodName, newBody);
|
|
1616
|
+
}
|
|
1617
|
+
async function astRemoveMethod(filePath, className, methodName) {
|
|
1618
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1619
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1620
|
+
return await editor.removeMethod(filePath, className, methodName);
|
|
1621
|
+
}
|
|
1622
|
+
async function astAddDecorator(filePath, className, decoratorCode) {
|
|
1623
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1624
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1625
|
+
return await editor.addDecorator(filePath, className, decoratorCode);
|
|
1626
|
+
}
|
|
1627
|
+
async function astAddInterface(filePath, interfaceCode) {
|
|
1628
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1629
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1630
|
+
return await editor.addInterface(filePath, interfaceCode);
|
|
1631
|
+
}
|
|
1632
|
+
async function astAddTypeAlias(filePath, typeCode) {
|
|
1633
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1634
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1635
|
+
return await editor.addTypeAlias(filePath, typeCode);
|
|
1636
|
+
}
|
|
1637
|
+
async function astAddFunction(filePath, functionCode) {
|
|
1638
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1639
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1640
|
+
return await editor.addFunction(filePath, functionCode);
|
|
1641
|
+
}
|
|
1642
|
+
async function astRemoveFunction(filePath, functionName) {
|
|
1643
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1644
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1645
|
+
return await editor.removeFunction(filePath, functionName);
|
|
1646
|
+
}
|
|
1647
|
+
async function astAddImport(filePath, importStatement) {
|
|
1648
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1649
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1650
|
+
return await editor.addImport(filePath, importStatement);
|
|
1651
|
+
}
|
|
1652
|
+
async function astRemoveImport(filePath, modulePath) {
|
|
1653
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1654
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1655
|
+
return await editor.removeImport(filePath, modulePath);
|
|
1656
|
+
}
|
|
1657
|
+
async function astOrganizeImports(filePath) {
|
|
1658
|
+
const editor = CodeEditorFactory.getEditor(filePath);
|
|
1659
|
+
if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
|
|
1660
|
+
return await editor.organizeImports(filePath);
|
|
1661
|
+
}
|
|
1221
1662
|
|
|
1222
1663
|
// src/core/agents/specification-agent.ts
|
|
1223
1664
|
var AGENT_TYPE2 = "specification_agent";
|
|
@@ -1232,29 +1673,39 @@ async function interactiveSpecificationAgent(options = {}) {
|
|
|
1232
1673
|
tui.intro("\u{1F3D7}\uFE0F Specification Agent");
|
|
1233
1674
|
const projectRoot = process.cwd();
|
|
1234
1675
|
let contextContent = "";
|
|
1235
|
-
const contextPath =
|
|
1236
|
-
if (
|
|
1676
|
+
const contextPath = path6.resolve(projectRoot, "_sharkrc", "project-context.md");
|
|
1677
|
+
if (fs5.existsSync(contextPath)) {
|
|
1237
1678
|
try {
|
|
1238
|
-
contextContent =
|
|
1239
|
-
tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(
|
|
1679
|
+
contextContent = fs5.readFileSync(contextPath, "utf-8");
|
|
1680
|
+
tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(path6.relative(projectRoot, contextPath))}`);
|
|
1240
1681
|
} catch (e) {
|
|
1241
1682
|
tui.log.warning(`Failed to read context file: ${e}`);
|
|
1242
1683
|
}
|
|
1243
1684
|
}
|
|
1244
1685
|
let briefingContent = "";
|
|
1245
|
-
if (options.briefingPath &&
|
|
1246
|
-
briefingContent =
|
|
1686
|
+
if (options.briefingPath && fs5.existsSync(options.briefingPath)) {
|
|
1687
|
+
briefingContent = fs5.readFileSync(options.briefingPath, "utf-8");
|
|
1247
1688
|
tui.log.info(`\u{1F4C4} Briefing loaded from: ${colors.dim(options.briefingPath)}`);
|
|
1248
1689
|
} else {
|
|
1249
|
-
const sharkRcBriefing =
|
|
1250
|
-
if (
|
|
1251
|
-
briefingContent =
|
|
1690
|
+
const sharkRcBriefing = path6.resolve(projectRoot, "_sharkrc", "briefing.md");
|
|
1691
|
+
if (fs5.existsSync(sharkRcBriefing)) {
|
|
1692
|
+
briefingContent = fs5.readFileSync(sharkRcBriefing, "utf-8");
|
|
1252
1693
|
tui.log.info(`\u{1F4C4} Standard Briefing loaded: ${colors.dim("_sharkrc/briefing.md")}`);
|
|
1253
1694
|
} else {
|
|
1254
1695
|
tui.log.info(colors.dim("\u2139\uFE0F No briefing file found in _sharkrc/briefing.md. Starting in interactive mode."));
|
|
1255
1696
|
}
|
|
1256
1697
|
}
|
|
1257
1698
|
let initialPrompt = "";
|
|
1699
|
+
if (options.initialContext) {
|
|
1700
|
+
initialPrompt += `
|
|
1701
|
+
\u26A0\uFE0F **CONTEXTO DE EXECU\xC7\xC3O ANTERIOR (HANDOVER)**:
|
|
1702
|
+
O Developer Agent estava executando tarefas. Aqui est\xE1 o resumo do que aconteceu at\xE9 agora:
|
|
1703
|
+
"""
|
|
1704
|
+
${options.initialContext}
|
|
1705
|
+
"""
|
|
1706
|
+
Analise esse hist\xF3rico acima. Se houve falha, proponha fixes na spec. Se o usu\xE1rio pediu mudan\xE7a, use isso como base.
|
|
1707
|
+
`;
|
|
1708
|
+
}
|
|
1258
1709
|
if (briefingContent) {
|
|
1259
1710
|
initialPrompt += `
|
|
1260
1711
|
Abaixo est\xE1 o **Briefing de Neg\xF3cio** ou Descri\xE7\xE3o da Tarefa.
|
|
@@ -1363,9 +1814,9 @@ ${result}
|
|
|
1363
1814
|
const BOM = "\uFEFF";
|
|
1364
1815
|
const contentToWrite = action.content || "";
|
|
1365
1816
|
const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
|
|
1366
|
-
const dir =
|
|
1367
|
-
if (!
|
|
1368
|
-
|
|
1817
|
+
const dir = path6.dirname(action.path);
|
|
1818
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
1819
|
+
fs5.writeFileSync(action.path, finalContent, { encoding: "utf-8" });
|
|
1369
1820
|
tui.log.success(`\u2705 Created: ${action.path}`);
|
|
1370
1821
|
executionResults += `[Action create_file(${action.path})]: Success
|
|
1371
1822
|
|
|
@@ -1383,7 +1834,7 @@ ${result}
|
|
|
1383
1834
|
`;
|
|
1384
1835
|
}
|
|
1385
1836
|
} else if (action.type === "delete_file") {
|
|
1386
|
-
|
|
1837
|
+
fs5.unlinkSync(action.path);
|
|
1387
1838
|
tui.log.success(`\u2705 Deleted: ${action.path}`);
|
|
1388
1839
|
executionResults += `[Action delete_file(${action.path})]: Success
|
|
1389
1840
|
|
|
@@ -1512,8 +1963,8 @@ async function callSpecAgentApi(prompt, onChunk, agentId) {
|
|
|
1512
1963
|
import { Command as Command2 } from "commander";
|
|
1513
1964
|
|
|
1514
1965
|
// src/core/agents/scan-agent.ts
|
|
1515
|
-
import
|
|
1516
|
-
import
|
|
1966
|
+
import fs6 from "fs";
|
|
1967
|
+
import path7 from "path";
|
|
1517
1968
|
var AGENT_TYPE3 = "scan_agent";
|
|
1518
1969
|
function getAgentId3() {
|
|
1519
1970
|
const config = ConfigManager.getInstance().getConfig();
|
|
@@ -1528,29 +1979,29 @@ async function interactiveScanAgent(options = {}) {
|
|
|
1528
1979
|
const projectRoot = process.cwd();
|
|
1529
1980
|
let outputFile;
|
|
1530
1981
|
if (options.output) {
|
|
1531
|
-
outputFile =
|
|
1982
|
+
outputFile = path7.resolve(process.cwd(), options.output);
|
|
1532
1983
|
} else {
|
|
1533
|
-
const outputDir =
|
|
1534
|
-
if (!
|
|
1535
|
-
const stat =
|
|
1984
|
+
const outputDir = path7.resolve(projectRoot, "_sharkrc");
|
|
1985
|
+
if (!fs6.existsSync(outputDir)) {
|
|
1986
|
+
const stat = fs6.existsSync(outputDir) ? fs6.statSync(outputDir) : null;
|
|
1536
1987
|
if (stat && stat.isFile()) {
|
|
1537
1988
|
tui.log.warning(`Warning: '_sharkrc' exists as a file. Using '_bmad/project-context' instead to avoid overwrite.`);
|
|
1538
|
-
const fallbackDir =
|
|
1539
|
-
if (!
|
|
1540
|
-
outputFile =
|
|
1989
|
+
const fallbackDir = path7.resolve(projectRoot, "_bmad/project-context");
|
|
1990
|
+
if (!fs6.existsSync(fallbackDir)) fs6.mkdirSync(fallbackDir, { recursive: true });
|
|
1991
|
+
outputFile = path7.join(fallbackDir, "project-context.md");
|
|
1541
1992
|
} else {
|
|
1542
|
-
|
|
1543
|
-
outputFile =
|
|
1993
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
1994
|
+
outputFile = path7.join(outputDir, "project-context.md");
|
|
1544
1995
|
}
|
|
1545
1996
|
} else {
|
|
1546
|
-
|
|
1547
|
-
outputFile =
|
|
1997
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
1998
|
+
outputFile = path7.join(outputDir, "project-context.md");
|
|
1548
1999
|
}
|
|
1549
2000
|
}
|
|
1550
2001
|
tui.log.info(`${t("commands.scan.scanningProject")} ${colors.bold(projectRoot)}`);
|
|
1551
2002
|
tui.log.info(`${t("commands.scan.outputTarget")} ${colors.bold(outputFile)}`);
|
|
1552
2003
|
tui.log.info(`${t("commands.scan.language")} ${colors.bold(language)}`);
|
|
1553
|
-
const configFileRelative =
|
|
2004
|
+
const configFileRelative = path7.relative(projectRoot, outputFile);
|
|
1554
2005
|
const initialTemplate = `# Project Context
|
|
1555
2006
|
|
|
1556
2007
|
## Overview
|
|
@@ -1583,9 +2034,9 @@ async function interactiveScanAgent(options = {}) {
|
|
|
1583
2034
|
## Key Patterns & Conventions
|
|
1584
2035
|
[TO BE ANALYZED]
|
|
1585
2036
|
`;
|
|
1586
|
-
if (!
|
|
2037
|
+
if (!fs6.existsSync(outputFile)) {
|
|
1587
2038
|
const BOM = "\uFEFF";
|
|
1588
|
-
|
|
2039
|
+
fs6.writeFileSync(outputFile, BOM + initialTemplate, { encoding: "utf-8" });
|
|
1589
2040
|
tui.log.success(`${t("commands.scan.templateCreated")} ${outputFile}`);
|
|
1590
2041
|
} else {
|
|
1591
2042
|
tui.log.info(t("commands.scan.fileExists"));
|
|
@@ -1883,11 +2334,11 @@ ${result}
|
|
|
1883
2334
|
|
|
1884
2335
|
`;
|
|
1885
2336
|
} else if (action.type === "create_file" || action.type === "modify_file") {
|
|
1886
|
-
const resolvedActionPath =
|
|
1887
|
-
const resolvedTargetPath =
|
|
2337
|
+
const resolvedActionPath = path7.resolve(action.path || "");
|
|
2338
|
+
const resolvedTargetPath = path7.resolve(targetPath);
|
|
1888
2339
|
let isTarget = resolvedActionPath === resolvedTargetPath;
|
|
1889
|
-
if (!isTarget &&
|
|
1890
|
-
tui.log.warning(t("commands.scan.targetRedirect").replace("{0}", action.path || "").replace("{1}",
|
|
2340
|
+
if (!isTarget && path7.basename(action.path || "") === "project-context.md") {
|
|
2341
|
+
tui.log.warning(t("commands.scan.targetRedirect").replace("{0}", action.path || "").replace("{1}", path7.relative(process.cwd(), targetPath)));
|
|
1891
2342
|
isTarget = true;
|
|
1892
2343
|
action.path = targetPath;
|
|
1893
2344
|
}
|
|
@@ -1897,16 +2348,16 @@ ${result}
|
|
|
1897
2348
|
if (action.type === "create_file") {
|
|
1898
2349
|
const contentToWrite = action.content || "";
|
|
1899
2350
|
const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
|
|
1900
|
-
|
|
2351
|
+
fs6.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
|
|
1901
2352
|
tui.log.success(t("commands.scan.generated").replace("{0}", finalPath));
|
|
1902
2353
|
fileCreated = true;
|
|
1903
2354
|
} else {
|
|
1904
|
-
if (
|
|
1905
|
-
const currentContent =
|
|
2355
|
+
if (fs6.existsSync(finalPath)) {
|
|
2356
|
+
const currentContent = fs6.readFileSync(finalPath, "utf-8");
|
|
1906
2357
|
if (action.target_content && currentContent.includes(action.target_content)) {
|
|
1907
2358
|
const newContent = currentContent.replace(action.target_content, action.content || "");
|
|
1908
2359
|
const finalContent = newContent.startsWith(BOM) ? newContent : BOM + newContent;
|
|
1909
|
-
|
|
2360
|
+
fs6.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
|
|
1910
2361
|
tui.log.success(t("commands.scan.updated").replace("{0}", finalPath));
|
|
1911
2362
|
fileCreated = true;
|
|
1912
2363
|
} else {
|
|
@@ -1935,7 +2386,7 @@ ${result}
|
|
|
1935
2386
|
}
|
|
1936
2387
|
}
|
|
1937
2388
|
if (fileCreated) {
|
|
1938
|
-
const currentContent =
|
|
2389
|
+
const currentContent = fs6.readFileSync(targetPath, "utf-8");
|
|
1939
2390
|
const pendingSections = [];
|
|
1940
2391
|
const lines = currentContent.split("\n");
|
|
1941
2392
|
let currentSection = "";
|
|
@@ -2027,8 +2478,8 @@ var scanCommand = new Command2("scan").description("Analyze the project and gene
|
|
|
2027
2478
|
import { Command as Command3 } from "commander";
|
|
2028
2479
|
|
|
2029
2480
|
// src/core/agents/developer-agent.ts
|
|
2030
|
-
import
|
|
2031
|
-
import
|
|
2481
|
+
import fs7 from "fs";
|
|
2482
|
+
import path8 from "path";
|
|
2032
2483
|
var AGENT_TYPE4 = "developer_agent";
|
|
2033
2484
|
async function validateTypeScript(filePath) {
|
|
2034
2485
|
try {
|
|
@@ -2041,32 +2492,6 @@ async function validateTypeScript(filePath) {
|
|
|
2041
2492
|
return { valid: false, error: e.message || "TypeScript validation failed" };
|
|
2042
2493
|
}
|
|
2043
2494
|
}
|
|
2044
|
-
function validateHtmlTagBalance(filePath) {
|
|
2045
|
-
const content = fs6.readFileSync(filePath, "utf-8");
|
|
2046
|
-
const stack = [];
|
|
2047
|
-
const tagRegex = /<\/?(\w+)(?:\s[^>]*)?\s*\/?>/g;
|
|
2048
|
-
const selfClosingTags = ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "param", "source", "track", "wbr"];
|
|
2049
|
-
let match;
|
|
2050
|
-
while ((match = tagRegex.exec(content)) !== null) {
|
|
2051
|
-
const fullTag = match[0];
|
|
2052
|
-
const tagName = match[1].toLowerCase();
|
|
2053
|
-
if (fullTag.startsWith("</")) {
|
|
2054
|
-
const expected = stack.pop();
|
|
2055
|
-
if (expected !== tagName) {
|
|
2056
|
-
return {
|
|
2057
|
-
valid: false,
|
|
2058
|
-
error: `Tag mismatch: expected </${expected}> but found </${tagName}> at position ${match.index}`
|
|
2059
|
-
};
|
|
2060
|
-
}
|
|
2061
|
-
} else if (!fullTag.endsWith("/>") && !selfClosingTags.includes(tagName)) {
|
|
2062
|
-
stack.push(tagName);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
if (stack.length > 0) {
|
|
2066
|
-
return { valid: false, error: `Unclosed tags: <${stack.join(">, <")}>` };
|
|
2067
|
-
}
|
|
2068
|
-
return { valid: true };
|
|
2069
|
-
}
|
|
2070
2495
|
function getAgentId4(overrideId) {
|
|
2071
2496
|
if (overrideId) return overrideId;
|
|
2072
2497
|
const config = ConfigManager.getInstance().getConfig();
|
|
@@ -2074,19 +2499,25 @@ function getAgentId4(overrideId) {
|
|
|
2074
2499
|
if (process.env.STACKSPOT_DEV_AGENT_ID) return process.env.STACKSPOT_DEV_AGENT_ID;
|
|
2075
2500
|
return "01KEQCGJ65YENRA4QBXVN1YFFX";
|
|
2076
2501
|
}
|
|
2077
|
-
function
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2502
|
+
async function interactiveDeveloperAgent(options = {}) {
|
|
2503
|
+
FileLogger.init();
|
|
2504
|
+
const agentId = getAgentId4();
|
|
2505
|
+
if (agentId === "PENDING_CONFIGURATION") {
|
|
2506
|
+
tui.log.error("\u274C STACKSPOT_DEV_AGENT_ID not configured in .env");
|
|
2507
|
+
return { success: false, summary: "Missing configuration." };
|
|
2081
2508
|
}
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2509
|
+
const projectRoot = process.cwd();
|
|
2510
|
+
let contextContent = "";
|
|
2511
|
+
const defaultContextPath = path8.resolve(projectRoot, "_sharkrc", "project-context.md");
|
|
2512
|
+
const specificContextPath = options.context ? path8.resolve(projectRoot, options.context) : defaultContextPath;
|
|
2513
|
+
if (fs7.existsSync(specificContextPath)) {
|
|
2514
|
+
try {
|
|
2515
|
+
contextContent = fs7.readFileSync(specificContextPath, "utf-8");
|
|
2516
|
+
} catch (e) {
|
|
2517
|
+
tui.log.warning(`Failed to read context file: ${e}`);
|
|
2518
|
+
}
|
|
2086
2519
|
}
|
|
2087
|
-
|
|
2088
|
-
}
|
|
2089
|
-
function buildSystemPrompt(state, contextContent, additionalInstructions = "") {
|
|
2520
|
+
const currentTask = options.taskInstruction || "Analyze the project and fix pending issues.";
|
|
2090
2521
|
let basePrompt = ``;
|
|
2091
2522
|
if (contextContent) {
|
|
2092
2523
|
basePrompt += `
|
|
@@ -2096,117 +2527,69 @@ ${contextContent}
|
|
|
2096
2527
|
-----------------------
|
|
2097
2528
|
`;
|
|
2098
2529
|
}
|
|
2099
|
-
if (
|
|
2530
|
+
if (options.history) {
|
|
2100
2531
|
basePrompt += `
|
|
2101
2532
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
\u26A0\uFE0F WORKFLOW:
|
|
2107
|
-
1. **Understand**: Clarify the goal with the user if needed.
|
|
2108
|
-
2. **Explore**: Use 'list_files'/'read_file' to find RELEVANT files for this specific task.
|
|
2109
|
-
3. **Specify**: Create 'tech-spec.md' referencing REAL file paths you found.
|
|
2110
|
-
|
|
2111
|
-
DO NOT create a spec based on guesses. Verify file existence before writing the plan.
|
|
2112
|
-
|
|
2113
|
-
Structure for 'tech-spec.md':
|
|
2114
|
-
\`\`\`markdown
|
|
2115
|
-
# Technical Spec: [Title]
|
|
2116
|
-
|
|
2117
|
-
## Goal
|
|
2118
|
-
[Brief description]
|
|
2119
|
-
|
|
2120
|
-
## Implementation Plan
|
|
2121
|
-
- [ ] Step 1: [Description]
|
|
2122
|
-
- [ ] Step 2: [Description]
|
|
2123
|
-
...
|
|
2124
|
-
\`\`\`
|
|
2125
|
-
User Request: "${additionalInstructions}"
|
|
2533
|
+
--- PREVIOUS EXECUTION SUMMARY ---
|
|
2534
|
+
${options.history}
|
|
2535
|
+
----------------------------------
|
|
2126
2536
|
`;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2537
|
+
}
|
|
2538
|
+
basePrompt += `
|
|
2129
2539
|
|
|
2130
2540
|
\u{1F7E2} EXECUTION MODE
|
|
2131
2541
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
\u{1F449} **CURRENT TASK**: "${state.nextTask}"
|
|
2135
|
-
|
|
2542
|
+
You are a highly skilled Developer Agent.
|
|
2543
|
+
\u{1F449} **CURRENT TASK**: "${currentTask}"
|
|
2136
2544
|
|
|
2137
|
-
|
|
2545
|
+
Your goal is to COMPLETE this specific task and then STOP.
|
|
2138
2546
|
1. Implement the necessary changes.
|
|
2139
2547
|
2. Verify (compile/test).
|
|
2140
|
-
3. **MANDATORY**:
|
|
2548
|
+
3. **MANDATORY**: When you are confident the task is done, output a final message starting with "TASK_COMPLETED:" followed by a brief technical summary of what you did.
|
|
2141
2549
|
`;
|
|
2142
|
-
|
|
2143
|
-
basePrompt += `
|
|
2144
|
-
|
|
2145
|
-
\u2728 ALL TASKS COMPLETED according to 'tech-spec.md'.
|
|
2146
|
-
|
|
2147
|
-
Ask the user if they want to add more tasks or finish the session.
|
|
2148
|
-
`;
|
|
2149
|
-
}
|
|
2150
|
-
return basePrompt;
|
|
2151
|
-
}
|
|
2152
|
-
async function interactiveDeveloperAgent(options = {}) {
|
|
2153
|
-
FileLogger.init();
|
|
2154
|
-
tui.intro("\u{1F988} Shark Dev Agent (Spec-Driven)");
|
|
2155
|
-
const agentId = getAgentId4();
|
|
2156
|
-
if (agentId === "PENDING_CONFIGURATION") {
|
|
2157
|
-
tui.log.error("\u274C STACKSPOT_DEV_AGENT_ID not configured in .env");
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
const projectRoot = process.cwd();
|
|
2161
|
-
let contextContent = "";
|
|
2162
|
-
const defaultContextPath = path7.resolve(projectRoot, "_sharkrc", "project-context.md");
|
|
2163
|
-
const specificContextPath = options.context ? path7.resolve(projectRoot, options.context) : defaultContextPath;
|
|
2164
|
-
if (fs6.existsSync(specificContextPath)) {
|
|
2165
|
-
try {
|
|
2166
|
-
contextContent = fs6.readFileSync(specificContextPath, "utf-8");
|
|
2167
|
-
tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(path7.relative(projectRoot, specificContextPath))}`);
|
|
2168
|
-
} catch (e) {
|
|
2169
|
-
tui.log.warning(`Failed to read context file: ${e}`);
|
|
2170
|
-
}
|
|
2171
|
-
} else {
|
|
2172
|
-
tui.log.warning(`\u26A0\uFE0F No context file found. Agent will run without pre-loaded context.`);
|
|
2173
|
-
}
|
|
2174
|
-
let specState = analyzeSpecState(projectRoot);
|
|
2175
|
-
let nextPrompt = buildSystemPrompt(specState, contextContent, options.task || "Start working.");
|
|
2550
|
+
let nextPrompt = basePrompt;
|
|
2176
2551
|
let keepGoing = true;
|
|
2177
2552
|
const spinner = tui.spinner();
|
|
2178
|
-
let
|
|
2553
|
+
let finalSummary = "";
|
|
2554
|
+
let isTaskCompleted = false;
|
|
2555
|
+
const conversationKey = options.taskId ? `dev_agent_${options.taskId}` : `dev_agent_${Date.now()}`;
|
|
2179
2556
|
let autoApprovals = {
|
|
2180
2557
|
files: false,
|
|
2181
2558
|
commands: false
|
|
2182
2559
|
};
|
|
2183
2560
|
while (keepGoing) {
|
|
2184
|
-
stepCount++;
|
|
2185
2561
|
try {
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
}
|
|
2189
|
-
tui.log.info(colors.warning(`\u{1F4CB} PLANNING: Creating tech-spec.md`));
|
|
2190
|
-
}
|
|
2191
|
-
spinner.start("Waiting for Shark Dev...");
|
|
2192
|
-
let lastResponse = null;
|
|
2193
|
-
await callDevAgentApi(nextPrompt, (chunk) => {
|
|
2194
|
-
}).then((resp) => {
|
|
2195
|
-
lastResponse = resp;
|
|
2196
|
-
});
|
|
2562
|
+
spinner.start("\u{1F988} Shark Dev working...");
|
|
2563
|
+
const lastResponse = await callDevAgentApi(nextPrompt, (chunk) => {
|
|
2564
|
+
}, conversationKey);
|
|
2197
2565
|
spinner.stop("Response received");
|
|
2198
|
-
if (lastResponse
|
|
2566
|
+
if (lastResponse) {
|
|
2199
2567
|
const response = lastResponse;
|
|
2568
|
+
const actions = response.actions || [];
|
|
2569
|
+
if (response.message && response.message.includes("TASK_COMPLETED:")) {
|
|
2570
|
+
isTaskCompleted = true;
|
|
2571
|
+
finalSummary = response.message.split("TASK_COMPLETED:")[1].trim();
|
|
2572
|
+
keepGoing = false;
|
|
2573
|
+
}
|
|
2574
|
+
if (actions.length === 0 && response.message && !isTaskCompleted) {
|
|
2575
|
+
tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
|
|
2576
|
+
console.log(response.message);
|
|
2577
|
+
const userReply = await tui.text({ message: "Your answer:" });
|
|
2578
|
+
if (tui.isCancel(userReply)) {
|
|
2579
|
+
keepGoing = false;
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
nextPrompt = userReply;
|
|
2583
|
+
}
|
|
2200
2584
|
let executionResults = "";
|
|
2201
2585
|
let waitingForUser = false;
|
|
2202
|
-
|
|
2203
|
-
for (const action of response.actions) {
|
|
2586
|
+
for (const action of actions) {
|
|
2204
2587
|
if (action.type === "talk_with_user") {
|
|
2205
2588
|
tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
|
|
2206
2589
|
console.log(action.content);
|
|
2207
|
-
waitingForUser = true;
|
|
2590
|
+
if (!isTaskCompleted) waitingForUser = true;
|
|
2208
2591
|
} else if (action.type === "list_files") {
|
|
2209
|
-
tui.log.info(`\u{1F4C2} Scanning
|
|
2592
|
+
tui.log.info(`\u{1F4C2} Scanning: ${colors.dim(action.path || ".")}`);
|
|
2210
2593
|
const result = handleListFiles(action.path || ".");
|
|
2211
2594
|
executionResults += `[Action list_files(${action.path}) Result]:
|
|
2212
2595
|
${result}
|
|
@@ -2220,7 +2603,6 @@ ${result}
|
|
|
2220
2603
|
|
|
2221
2604
|
`;
|
|
2222
2605
|
} else if (action.type === "search_file") {
|
|
2223
|
-
tui.log.info(`\u{1F50D} Searching: ${colors.dim(action.path || "")}`);
|
|
2224
2606
|
const result = handleSearchFile(action.path || "");
|
|
2225
2607
|
executionResults += `[Action search_file(${action.path}) Result]:
|
|
2226
2608
|
${result}
|
|
@@ -2229,26 +2611,16 @@ ${result}
|
|
|
2229
2611
|
} else if (action.type === "run_command") {
|
|
2230
2612
|
const cmd = action.command || "";
|
|
2231
2613
|
tui.log.info(`\u{1F4BB} Executing: ${colors.dim(cmd)}`);
|
|
2232
|
-
let approved =
|
|
2233
|
-
if (
|
|
2234
|
-
approved = true;
|
|
2235
|
-
tui.log.success(`\u26A1 Auto-Approved Command: ${cmd}`);
|
|
2236
|
-
} else {
|
|
2614
|
+
let approved = autoApprovals.commands;
|
|
2615
|
+
if (!approved) {
|
|
2237
2616
|
const choice = await tui.select({
|
|
2238
2617
|
message: `Execute: ${cmd}?`,
|
|
2239
|
-
options: [
|
|
2240
|
-
{ value: "yes", label: "Yes (Execute once)" },
|
|
2241
|
-
{ value: "always", label: "Yes (Output & Auto-Approve Commands for Session)" },
|
|
2242
|
-
{ value: "no", label: "No (Skip)" }
|
|
2243
|
-
]
|
|
2618
|
+
options: [{ value: "yes", label: "Yes" }, { value: "always", label: "Yes (Auto-Approve Session)" }, { value: "no", label: "No" }]
|
|
2244
2619
|
});
|
|
2245
2620
|
if (choice === "always") {
|
|
2246
2621
|
autoApprovals.commands = true;
|
|
2247
2622
|
approved = true;
|
|
2248
|
-
|
|
2249
|
-
} else if (choice === "yes") {
|
|
2250
|
-
approved = true;
|
|
2251
|
-
}
|
|
2623
|
+
} else if (choice === "yes") approved = true;
|
|
2252
2624
|
}
|
|
2253
2625
|
if (approved) {
|
|
2254
2626
|
const result = await handleRunCommand(cmd);
|
|
@@ -2262,302 +2634,159 @@ ${result}
|
|
|
2262
2634
|
`;
|
|
2263
2635
|
}
|
|
2264
2636
|
} else if (["create_file", "modify_file"].includes(action.type)) {
|
|
2265
|
-
const isCreate = action.type === "create_file";
|
|
2266
2637
|
const filePath = action.path || "";
|
|
2267
|
-
tui.log.warning(`
|
|
2268
|
-
|
|
2269
|
-
if (
|
|
2270
|
-
const preview = action.content.length > 500 ? action.content.substring(0, 500) + "... (truncated)" : action.content;
|
|
2271
|
-
console.log(colors.dim("--- Content ---\n") + preview + "\n" + colors.dim("---------------"));
|
|
2272
|
-
}
|
|
2273
|
-
if (!isCreate && action.line_range && !action.confirmed) {
|
|
2274
|
-
tui.log.info("\u{1F6E1}\uFE0F Generation Preview for Agent Verification...");
|
|
2275
|
-
const [start, end] = action.line_range;
|
|
2276
|
-
const previewDiff = await generateFilePreview(filePath, start, end, action.content || "");
|
|
2277
|
-
executionResults += `[Action modify_file]: PENDING CONFIRMATION.
|
|
2278
|
-
|
|
2279
|
-
${previewDiff}
|
|
2280
|
-
|
|
2281
|
-
`;
|
|
2282
|
-
executionResults += `USER/SYSTEM INSTRUCTION: Please review the above preview carefully.
|
|
2283
|
-
1. If the context and changes look correct, call 'modify_file' again with the SAME parameters AND "confirmed": true.
|
|
2284
|
-
2. If the context is wrong (e.g. wrong line numbers), call 'read_file' to check line numbers again, then correct your request.
|
|
2285
|
-
|
|
2286
|
-
`;
|
|
2287
|
-
tui.log.warning("\u26A0\uFE0F Sent Preview to Agent for confirmation first.");
|
|
2288
|
-
waitingForUser = false;
|
|
2289
|
-
continue;
|
|
2290
|
-
}
|
|
2291
|
-
let approved = false;
|
|
2292
|
-
if (autoApprovals.files) {
|
|
2293
|
-
approved = true;
|
|
2294
|
-
tui.log.success(`\u26A1 Auto-Approved File Action: ${filePath}`);
|
|
2295
|
-
} else {
|
|
2638
|
+
tui.log.warning(`\u{1F4DD} ${action.type === "create_file" ? "CREATE" : "MODIFY"}: ${colors.bold(filePath)}`);
|
|
2639
|
+
let approved = autoApprovals.files;
|
|
2640
|
+
if (!approved) {
|
|
2296
2641
|
const choice = await tui.select({
|
|
2297
2642
|
message: `Approve changes to ${filePath}?`,
|
|
2298
|
-
options: [
|
|
2299
|
-
{ value: "yes", label: "Yes (Approve once)" },
|
|
2300
|
-
{ value: "always", label: "Yes (Approve & Auto-Approve FILES for Session)" },
|
|
2301
|
-
{ value: "no", label: "No (Skip)" }
|
|
2302
|
-
]
|
|
2643
|
+
options: [{ value: "yes", label: "Yes" }, { value: "always", label: "Yes (Auto-Approve Session)" }, { value: "no", label: "No" }]
|
|
2303
2644
|
});
|
|
2304
2645
|
if (choice === "always") {
|
|
2305
2646
|
autoApprovals.files = true;
|
|
2306
2647
|
approved = true;
|
|
2307
|
-
|
|
2308
|
-
} else if (choice === "yes") {
|
|
2309
|
-
approved = true;
|
|
2310
|
-
}
|
|
2648
|
+
} else if (choice === "yes") approved = true;
|
|
2311
2649
|
}
|
|
2312
2650
|
if (approved) {
|
|
2313
|
-
if (
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const BOM = "\uFEFF";
|
|
2319
|
-
const contentToWrite = action.content || "";
|
|
2320
|
-
const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
|
|
2321
|
-
fs6.writeFileSync(targetPath, finalContent, { encoding: "utf-8" });
|
|
2322
|
-
tui.log.success(`\u2705 Created: ${filePath}`);
|
|
2323
|
-
executionResults += `[Action create_file(${filePath})]: Success
|
|
2324
|
-
|
|
2325
|
-
`;
|
|
2326
|
-
if (filePath.endsWith("tech-spec.md")) specUpdated = true;
|
|
2327
|
-
const config = ConfigManager.getInstance().getConfig();
|
|
2328
|
-
const validationEnabled = config.validation?.enablePostSaveValidation ?? true;
|
|
2329
|
-
if (validationEnabled) {
|
|
2330
|
-
const ext = path7.extname(filePath);
|
|
2331
|
-
if ([".ts", ".tsx"].includes(ext)) {
|
|
2332
|
-
tui.log.info("\u{1F50D} Validating TypeScript...");
|
|
2333
|
-
const validation = await validateTypeScript(filePath);
|
|
2334
|
-
if (!validation.valid) {
|
|
2335
|
-
tui.log.error("\u274C TypeScript validation failed");
|
|
2336
|
-
executionResults += `
|
|
2337
|
-
[TYPESCRIPT VALIDATION FAILED]:
|
|
2338
|
-
${validation.error}
|
|
2339
|
-
|
|
2340
|
-
`;
|
|
2341
|
-
executionResults += `CRITICAL: The file has been saved but contains errors. Read the file again to verify line numbers before attempting to fix. Do not guess line numbers.
|
|
2342
|
-
`;
|
|
2343
|
-
} else {
|
|
2344
|
-
tui.log.success("\u2705 TypeScript OK");
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
if (ext === ".html") {
|
|
2348
|
-
tui.log.info("\u{1F50D} Validating HTML...");
|
|
2349
|
-
const validation = validateHtmlTagBalance(filePath);
|
|
2350
|
-
if (!validation.valid) {
|
|
2351
|
-
tui.log.error("\u274C HTML validation failed");
|
|
2352
|
-
executionResults += `
|
|
2353
|
-
[HTML VALIDATION FAILED]:
|
|
2354
|
-
${validation.error}
|
|
2355
|
-
|
|
2356
|
-
`;
|
|
2357
|
-
executionResults += `CRITICAL: Fix these errors before proceeding to next task.
|
|
2358
|
-
`;
|
|
2359
|
-
} else {
|
|
2360
|
-
tui.log.success("\u2705 HTML OK");
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
} else {
|
|
2365
|
-
let success = false;
|
|
2366
|
-
tui.log.info(`\u{1F50D} Debug modify_file: line_range=${JSON.stringify(action.line_range)}, type=${typeof action.line_range}, isArray=${Array.isArray(action.line_range)}`);
|
|
2367
|
-
if (action.line_range && Array.isArray(action.line_range) && action.line_range.length === 2) {
|
|
2368
|
-
const [start, end] = action.line_range;
|
|
2369
|
-
success = replaceLineRange(filePath, start, end, action.content || "", tui);
|
|
2370
|
-
} else if (action.target_content) {
|
|
2371
|
-
success = startSmartReplace(filePath, action.content || "", action.target_content, tui);
|
|
2372
|
-
} else {
|
|
2373
|
-
tui.log.error("\u274C Missing line_range (recommended) or target_content for modification.");
|
|
2374
|
-
executionResults += `[Action modify_file]: Failed. Missing line_range or target_content.
|
|
2375
|
-
|
|
2376
|
-
`;
|
|
2377
|
-
}
|
|
2378
|
-
if (success) {
|
|
2379
|
-
executionResults += `[Action modify_file(${filePath})]: Success
|
|
2651
|
+
if (action.type === "create_file") {
|
|
2652
|
+
const dir = path8.dirname(path8.resolve(projectRoot, filePath));
|
|
2653
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
2654
|
+
fs7.writeFileSync(path8.resolve(projectRoot, filePath), action.content || "", "utf-8");
|
|
2655
|
+
executionResults += `[Action create_file]: Success
|
|
2380
2656
|
|
|
2381
2657
|
`;
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
if ([".ts", ".tsx"].includes(ext)) {
|
|
2388
|
-
tui.log.info("\u{1F50D} Validating TypeScript...");
|
|
2389
|
-
const validation = await validateTypeScript(filePath);
|
|
2390
|
-
if (!validation.valid) {
|
|
2391
|
-
tui.log.error("\u274C TypeScript validation failed");
|
|
2392
|
-
executionResults += `
|
|
2393
|
-
[TYPESCRIPT VALIDATION FAILED]:
|
|
2394
|
-
${validation.error}
|
|
2658
|
+
} else {
|
|
2659
|
+
let success = false;
|
|
2660
|
+
if (action.line_range) success = replaceLineRange(filePath, action.line_range[0], action.line_range[1], action.content || "", tui);
|
|
2661
|
+
else if (action.target_content) success = startSmartReplace(filePath, action.content || "", action.target_content, tui);
|
|
2662
|
+
executionResults += success ? `[Action modify_file]: Success
|
|
2395
2663
|
|
|
2396
|
-
|
|
2397
|
-
executionResults += `CRITICAL: Fix these errors before proceeding to next task.
|
|
2398
|
-
`;
|
|
2399
|
-
} else {
|
|
2400
|
-
tui.log.success("\u2705 TypeScript OK");
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
if (ext === ".html") {
|
|
2404
|
-
tui.log.info("\u{1F50D} Validating HTML...");
|
|
2405
|
-
const validation = validateHtmlTagBalance(filePath);
|
|
2406
|
-
if (!validation.valid) {
|
|
2407
|
-
tui.log.error("\u274C HTML validation failed");
|
|
2408
|
-
executionResults += `
|
|
2409
|
-
[HTML VALIDATION FAILED]:
|
|
2410
|
-
${validation.error}
|
|
2664
|
+
` : `[Action modify_file]: Failed
|
|
2411
2665
|
|
|
2412
2666
|
`;
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
tui.log.success("\u2705 HTML OK");
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
} else if (!action.line_range && !action.target_content) {
|
|
2421
|
-
} else {
|
|
2422
|
-
executionResults += `[Action modify_file(${filePath})]: FAILED. Target content not found or ambiguous. Read the file again to ensure accuracy.
|
|
2667
|
+
}
|
|
2668
|
+
const val = await validateTypeScript(path8.resolve(projectRoot, filePath));
|
|
2669
|
+
if (!val.valid) executionResults += `[Validation Failed]: ${val.error}
|
|
2423
2670
|
|
|
2424
2671
|
`;
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
2672
|
} else {
|
|
2429
|
-
tui.log.error("\u274C Denied.");
|
|
2430
2673
|
executionResults += `[Action ${action.type}]: User Denied.
|
|
2431
2674
|
|
|
2432
2675
|
`;
|
|
2433
2676
|
}
|
|
2434
|
-
} else if (action.type
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
action.
|
|
2438
|
-
|
|
2439
|
-
action.
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2677
|
+
} else if (action.type.startsWith("ast_")) {
|
|
2678
|
+
try {
|
|
2679
|
+
let result = "";
|
|
2680
|
+
if (action.type === "ast_list_structure") {
|
|
2681
|
+
result = await astListStructure(action.path || "");
|
|
2682
|
+
} else if (action.type === "ast_get_method") {
|
|
2683
|
+
result = await astGetMethod(action.path || "", action.class_name || "", action.method_name || "");
|
|
2684
|
+
} else if (action.type === "ast_add_method") {
|
|
2685
|
+
const success = await astAddMethod(action.path || "", action.class_name || "", action.method_code || "");
|
|
2686
|
+
result = success ? "Method added successfully." : "Failed to add method.";
|
|
2687
|
+
} else if (action.type === "ast_modify_method") {
|
|
2688
|
+
const success = await astModifyMethod(action.path || "", action.class_name || "", action.method_name || "", action.new_body || "");
|
|
2689
|
+
result = success ? "Method modified successfully." : "Failed to modify method.";
|
|
2690
|
+
} else if (action.type === "ast_remove_method") {
|
|
2691
|
+
const success = await astRemoveMethod(action.path || "", action.class_name || "", action.method_name || "");
|
|
2692
|
+
result = success ? "Method removed successfully." : "Failed to remove method.";
|
|
2693
|
+
} else if (action.type === "ast_add_class") {
|
|
2694
|
+
const success = await astAddClass(action.path || "", action.class_name || "", action.extends_class || void 0, action.implements_interfaces || void 0);
|
|
2695
|
+
result = success ? "Class added successfully." : "Failed to add class.";
|
|
2696
|
+
} else if (action.type === "ast_add_property") {
|
|
2697
|
+
const success = await astAddProperty(action.path || "", action.class_name || "", action.property_code || "");
|
|
2698
|
+
result = success ? "Property added successfully." : "Failed to add property.";
|
|
2699
|
+
} else if (action.type === "ast_remove_property") {
|
|
2700
|
+
const success = await astRemoveProperty(action.path || "", action.class_name || "", action.property_name || "");
|
|
2701
|
+
result = success ? "Property removed successfully." : "Failed to remove property.";
|
|
2702
|
+
} else if (action.type === "ast_add_decorator") {
|
|
2703
|
+
const success = await astAddDecorator(action.path || "", action.class_name || "", action.decorator_code || "");
|
|
2704
|
+
result = success ? "Decorator added successfully." : "Failed to add decorator.";
|
|
2705
|
+
} else if (action.type === "ast_add_interface") {
|
|
2706
|
+
const success = await astAddInterface(action.path || "", action.interface_code || "");
|
|
2707
|
+
result = success ? "Interface added successfully." : "Failed to add interface.";
|
|
2708
|
+
} else if (action.type === "ast_add_type_alias") {
|
|
2709
|
+
const success = await astAddTypeAlias(action.path || "", action.type_code || "");
|
|
2710
|
+
result = success ? "Type alias added successfully." : "Failed to add type alias.";
|
|
2711
|
+
} else if (action.type === "ast_add_function") {
|
|
2712
|
+
const success = await astAddFunction(action.path || "", action.function_code || "");
|
|
2713
|
+
result = success ? "Function added successfully." : "Failed to add function.";
|
|
2714
|
+
} else if (action.type === "ast_remove_function") {
|
|
2715
|
+
const success = await astRemoveFunction(action.path || "", action.function_name || "");
|
|
2716
|
+
result = success ? "Function removed successfully." : "Failed to remove function.";
|
|
2717
|
+
} else if (action.type === "ast_add_import") {
|
|
2718
|
+
const success = await astAddImport(action.path || "", action.import_statement || "");
|
|
2719
|
+
result = success ? "Import added successfully." : "Failed to add import.";
|
|
2720
|
+
} else if (action.type === "ast_remove_import") {
|
|
2721
|
+
const success = await astRemoveImport(action.path || "", action.module_path || "");
|
|
2722
|
+
result = success ? "Import removed successfully." : "Failed to remove import.";
|
|
2723
|
+
} else if (action.type === "ast_organize_imports") {
|
|
2724
|
+
const success = await astOrganizeImports(action.path || "");
|
|
2725
|
+
result = success ? "Imports organized successfully." : "Failed to organize imports.";
|
|
2726
|
+
} else {
|
|
2727
|
+
result = `Unknown AST action: ${action.type}`;
|
|
2728
|
+
}
|
|
2729
|
+
executionResults += `[Action ${action.type} Result]:
|
|
2444
2730
|
${result}
|
|
2445
2731
|
|
|
2446
2732
|
`;
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
tui.log.info(`Pattern: ${colors.primary(action.pattern || "")}`);
|
|
2451
|
-
tui.log.info(`Fix: ${colors.success(action.fix || "")}`);
|
|
2452
|
-
const approved = await tui.confirm({ message: "Execute this AST modification?" });
|
|
2453
|
-
if (approved) {
|
|
2454
|
-
const success = await astGrepRewrite(
|
|
2455
|
-
action.pattern || "",
|
|
2456
|
-
action.fix || "",
|
|
2457
|
-
targetPath,
|
|
2458
|
-
action.language || "typescript",
|
|
2459
|
-
tui
|
|
2460
|
-
);
|
|
2461
|
-
if (success) {
|
|
2462
|
-
executionResults += `[Action modify_ast]: Success.
|
|
2733
|
+
tui.log.info(`\u26A1 AST Action ${colors.dim(action.type)}: ${result}`);
|
|
2734
|
+
} catch (e) {
|
|
2735
|
+
executionResults += `[Action ${action.type} Failed]: ${e.message}
|
|
2463
2736
|
|
|
2464
2737
|
`;
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
[TYPESCRIPT VALIDATION FAILED]:
|
|
2472
|
-
${validation.error}
|
|
2738
|
+
tui.log.error(`\u274C AST Action Error: ${e.message}`);
|
|
2739
|
+
}
|
|
2740
|
+
} else if (action.type === "search_ast") {
|
|
2741
|
+
const result = await astGrepSearch(action.pattern || "", action.path || "", action.language || "typescript", tui);
|
|
2742
|
+
executionResults += `[Action search_ast Result]:
|
|
2743
|
+
${result}
|
|
2473
2744
|
|
|
2474
2745
|
`;
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
executionResults += `[Action modify_ast]: Failed. Check logs.
|
|
2746
|
+
} else if (action.type === "modify_ast") {
|
|
2747
|
+
const success = await astGrepRewrite(action.pattern || "", action.fix || "", action.path || "", action.language || "typescript", tui);
|
|
2748
|
+
executionResults += success ? `[Action modify_ast]: Success
|
|
2479
2749
|
|
|
2480
|
-
|
|
2481
|
-
}
|
|
2482
|
-
} else {
|
|
2483
|
-
tui.log.error("\u274C Denied.");
|
|
2484
|
-
executionResults += `[Action modify_ast]: User Denied.
|
|
2750
|
+
` : `[Action modify_ast]: Failed
|
|
2485
2751
|
|
|
2486
2752
|
`;
|
|
2487
|
-
}
|
|
2488
2753
|
}
|
|
2489
2754
|
}
|
|
2490
|
-
const previousState = specState;
|
|
2491
|
-
specState = analyzeSpecState(projectRoot);
|
|
2492
|
-
let systemInjection = "";
|
|
2493
2755
|
if (executionResults) {
|
|
2494
|
-
if (previousState.status === "PENDING" && specState.status === "PENDING" && previousState.nextTask !== specState.nextTask) {
|
|
2495
|
-
systemInjection = `
|
|
2496
|
-
\u{1F389} Task "${previousState.nextTask}" COMPLETED! Next up: "${specState.nextTask}".
|
|
2497
|
-
`;
|
|
2498
|
-
} else if (previousState.status === "PENDING" && specState.status === "PENDING" && previousState.nextTask === specState.nextTask) {
|
|
2499
|
-
if (!specUpdated && stepCount % 3 === 0) {
|
|
2500
|
-
systemInjection = `
|
|
2501
|
-
Reminder: You are still working on "${specState.nextTask}". Don't forget to mark it [x] in 'tech-spec.md' when done.
|
|
2502
|
-
`;
|
|
2503
|
-
}
|
|
2504
|
-
} else if (previousState.status === "MISSING" && specState.status === "PENDING") {
|
|
2505
|
-
systemInjection = `
|
|
2506
|
-
\u2705 Spec Created! Starting execution of: "${specState.nextTask}".
|
|
2507
|
-
`;
|
|
2508
|
-
}
|
|
2509
2756
|
if (waitingForUser) {
|
|
2510
2757
|
const userReply = await tui.text({ message: "Your answer:" });
|
|
2511
2758
|
if (tui.isCancel(userReply)) {
|
|
2512
2759
|
keepGoing = false;
|
|
2513
2760
|
break;
|
|
2514
2761
|
}
|
|
2515
|
-
nextPrompt = `${executionResults}
|
|
2762
|
+
nextPrompt = `${executionResults}
|
|
2516
2763
|
User Reply: ${userReply}`;
|
|
2517
2764
|
} else {
|
|
2518
|
-
nextPrompt = `${executionResults}
|
|
2519
|
-
[System]: Continue.`;
|
|
2765
|
+
nextPrompt = `${executionResults}
|
|
2766
|
+
[System]: Continue execution. If finished, output "TASK_COMPLETED: summary".`;
|
|
2520
2767
|
tui.log.info(colors.dim("Processing results..."));
|
|
2521
2768
|
}
|
|
2769
|
+
} else if (!keepGoing) {
|
|
2522
2770
|
} else if (waitingForUser) {
|
|
2523
|
-
const userReply = await tui.text({ message: "Your answer:" });
|
|
2524
|
-
if (tui.isCancel(userReply)) {
|
|
2525
|
-
keepGoing = false;
|
|
2526
|
-
break;
|
|
2527
|
-
}
|
|
2528
|
-
nextPrompt = userReply;
|
|
2529
2771
|
} else {
|
|
2530
|
-
if (
|
|
2531
|
-
tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
|
|
2532
|
-
console.log(response.message);
|
|
2533
|
-
const userReply = await tui.text({ message: "Your answer:" });
|
|
2534
|
-
if (tui.isCancel(userReply)) {
|
|
2535
|
-
keepGoing = false;
|
|
2536
|
-
} else {
|
|
2537
|
-
nextPrompt = userReply;
|
|
2538
|
-
}
|
|
2539
|
-
} else {
|
|
2540
|
-
tui.log.warning("Agent took no actions and sent no message.");
|
|
2541
|
-
nextPrompt = "Please proceed or ask for clarification.";
|
|
2542
|
-
}
|
|
2772
|
+
if (!isTaskCompleted && actions.length > 0) nextPrompt = "Please continue.";
|
|
2543
2773
|
}
|
|
2544
2774
|
} else {
|
|
2545
|
-
tui.log.warning("
|
|
2546
|
-
nextPrompt = "Error: No valid actions returned. Please try again with JSON format.";
|
|
2775
|
+
tui.log.warning("No response received from agent.");
|
|
2547
2776
|
}
|
|
2548
2777
|
} catch (e) {
|
|
2549
|
-
spinner.stop("Error");
|
|
2550
2778
|
tui.log.error(e.message);
|
|
2551
|
-
FileLogger.log("DEV_AGENT", "Main Loop Error", e);
|
|
2552
2779
|
keepGoing = false;
|
|
2780
|
+
return { success: false, summary: `Error: ${e.message}` };
|
|
2553
2781
|
}
|
|
2554
2782
|
}
|
|
2555
|
-
tui.
|
|
2783
|
+
tui.log.success("\u2705 Task Scope Completed");
|
|
2784
|
+
return { success: true, summary: finalSummary || "Task completed without summary." };
|
|
2556
2785
|
}
|
|
2557
|
-
async function callDevAgentApi(prompt, onChunk) {
|
|
2786
|
+
async function callDevAgentApi(prompt, onChunk, conversationKey = AGENT_TYPE4) {
|
|
2558
2787
|
const realm = await getActiveRealm();
|
|
2559
2788
|
const token = await ensureValidToken(realm);
|
|
2560
|
-
const conversationId = await conversationManager.getConversationId(
|
|
2789
|
+
const conversationId = await conversationManager.getConversationId(conversationKey);
|
|
2561
2790
|
const payload = {
|
|
2562
2791
|
user_prompt: prompt,
|
|
2563
2792
|
streaming: true,
|
|
@@ -2586,22 +2815,248 @@ async function callDevAgentApi(prompt, onChunk) {
|
|
|
2586
2815
|
});
|
|
2587
2816
|
const parsed = parseAgentResponse(raw);
|
|
2588
2817
|
if (parsed.conversation_id) {
|
|
2589
|
-
await conversationManager.saveConversationId(
|
|
2818
|
+
await conversationManager.saveConversationId(conversationKey, parsed.conversation_id);
|
|
2590
2819
|
}
|
|
2591
2820
|
return parsed;
|
|
2592
2821
|
}
|
|
2593
2822
|
|
|
2823
|
+
// src/core/workflow/task-manager.ts
|
|
2824
|
+
import fs8 from "fs";
|
|
2825
|
+
import path9 from "path";
|
|
2826
|
+
var TaskManager = class {
|
|
2827
|
+
projectRoot;
|
|
2828
|
+
specPath;
|
|
2829
|
+
constructor(projectRoot = process.cwd()) {
|
|
2830
|
+
this.projectRoot = projectRoot;
|
|
2831
|
+
this.specPath = path9.resolve(this.projectRoot, "tech-spec.md");
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Reads the tech-spec.md file and analyzes its current state.
|
|
2835
|
+
*/
|
|
2836
|
+
analyzeSpecState() {
|
|
2837
|
+
if (!fs8.existsSync(this.specPath)) {
|
|
2838
|
+
return { status: "MISSING", allTasks: [] };
|
|
2839
|
+
}
|
|
2840
|
+
const content = fs8.readFileSync(this.specPath, "utf-8");
|
|
2841
|
+
const lines = content.split("\n");
|
|
2842
|
+
const tasks = [];
|
|
2843
|
+
let taskIndex = 1;
|
|
2844
|
+
lines.forEach((line, index) => {
|
|
2845
|
+
const trimmed = line.trim();
|
|
2846
|
+
const pendingMatch = trimmed.match(/^- \[ \] (.*)/);
|
|
2847
|
+
const completedMatch = trimmed.match(/^- \[x\] (.*)/i);
|
|
2848
|
+
const progressMatch = trimmed.match(/^- \[\/\] (.*)/);
|
|
2849
|
+
if (pendingMatch) {
|
|
2850
|
+
tasks.push({
|
|
2851
|
+
id: `task-${taskIndex++}`,
|
|
2852
|
+
description: pendingMatch[1].trim(),
|
|
2853
|
+
status: "PENDING",
|
|
2854
|
+
line_number: index
|
|
2855
|
+
});
|
|
2856
|
+
} else if (completedMatch) {
|
|
2857
|
+
tasks.push({
|
|
2858
|
+
id: `task-${taskIndex++}`,
|
|
2859
|
+
description: completedMatch[1].trim(),
|
|
2860
|
+
status: "COMPLETED",
|
|
2861
|
+
line_number: index
|
|
2862
|
+
});
|
|
2863
|
+
} else if (progressMatch) {
|
|
2864
|
+
tasks.push({
|
|
2865
|
+
id: `task-${taskIndex++}`,
|
|
2866
|
+
description: progressMatch[1].trim(),
|
|
2867
|
+
status: "IN_PROGRESS",
|
|
2868
|
+
line_number: index
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
let nextTask = tasks.find((t2) => t2.status === "IN_PROGRESS");
|
|
2873
|
+
if (!nextTask) {
|
|
2874
|
+
nextTask = tasks.find((t2) => t2.status === "PENDING");
|
|
2875
|
+
}
|
|
2876
|
+
const status = !nextTask && tasks.length > 0 && tasks.every((t2) => t2.status === "COMPLETED") ? "COMPLETED" : "PENDING";
|
|
2877
|
+
return {
|
|
2878
|
+
status: tasks.length === 0 ? "MISSING" : status,
|
|
2879
|
+
// Empty file is effectively "pending creation" but we treat as missing content logic elsewhere
|
|
2880
|
+
nextTask,
|
|
2881
|
+
allTasks: tasks
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Marks a specific task as COMPLETED in the file.
|
|
2886
|
+
* Uses line-based replacement to be safe.
|
|
2887
|
+
*/
|
|
2888
|
+
markTaskAsDone(taskId) {
|
|
2889
|
+
const state = this.analyzeSpecState();
|
|
2890
|
+
const task = state.allTasks.find((t2) => t2.id === taskId);
|
|
2891
|
+
if (!task) {
|
|
2892
|
+
console.error(`Task ${taskId} not found.`);
|
|
2893
|
+
return false;
|
|
2894
|
+
}
|
|
2895
|
+
const content = fs8.readFileSync(this.specPath, "utf-8");
|
|
2896
|
+
const lines = content.split("\n");
|
|
2897
|
+
const targetLine = lines[task.line_number];
|
|
2898
|
+
if (!targetLine.includes(task.description)) {
|
|
2899
|
+
console.error(`Concurrency Error: Task line content mistmatch. Expected "${task.description}" at line ${task.line_number}.`);
|
|
2900
|
+
return false;
|
|
2901
|
+
}
|
|
2902
|
+
const newLine = targetLine.replace("- [ ]", "- [x]").replace("- [/]", "- [x]");
|
|
2903
|
+
lines[task.line_number] = newLine;
|
|
2904
|
+
fs8.writeFileSync(this.specPath, lines.join("\n"), "utf-8");
|
|
2905
|
+
return true;
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Marks a task as IN_PROGRESS.
|
|
2909
|
+
*/
|
|
2910
|
+
markTaskInProgress(taskId) {
|
|
2911
|
+
const state = this.analyzeSpecState();
|
|
2912
|
+
const task = state.allTasks.find((t2) => t2.id === taskId);
|
|
2913
|
+
if (!task) return false;
|
|
2914
|
+
const content = fs8.readFileSync(this.specPath, "utf-8");
|
|
2915
|
+
const lines = content.split("\n");
|
|
2916
|
+
let targetLine = lines[task.line_number];
|
|
2917
|
+
targetLine = targetLine.replace("- [ ]", "- [/]");
|
|
2918
|
+
lines[task.line_number] = targetLine;
|
|
2919
|
+
fs8.writeFileSync(this.specPath, lines.join("\n"), "utf-8");
|
|
2920
|
+
return true;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Completely updates the spec file content (used by Spec Agent).
|
|
2924
|
+
*/
|
|
2925
|
+
updateSpecContent(newContent) {
|
|
2926
|
+
fs8.writeFileSync(this.specPath, newContent, "utf-8");
|
|
2927
|
+
}
|
|
2928
|
+
getSpecPath() {
|
|
2929
|
+
return this.specPath;
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2594
2933
|
// src/commands/dev.ts
|
|
2595
|
-
var devCommand = new Command3("dev").description("Starts the Shark Developer Agent (Shark Dev)").option("-t, --task <type>", "Initial task description").option("-c, --context <path>", "Path to custom context file").action(async (options) => {
|
|
2596
|
-
|
|
2934
|
+
var devCommand = new Command3("dev").description("Starts the Shark Developer Agent (Shark Dev Orchestration V2)").option("-t, --task <type>", "Initial task description (Quick Mode)").option("-c, --context <path>", "Path to custom context file").action(async (options) => {
|
|
2935
|
+
const taskManager = new TaskManager(process.cwd());
|
|
2936
|
+
let state = taskManager.analyzeSpecState();
|
|
2937
|
+
if (state.status === "MISSING") {
|
|
2938
|
+
tui.log.warning("\u{1F4CB} No tech-spec.md found.");
|
|
2939
|
+
const confirm = await tui.confirm({ message: "Create a new Specification (Tech Spec)?" });
|
|
2940
|
+
if (confirm) {
|
|
2941
|
+
await interactiveSpecificationAgent({
|
|
2942
|
+
briefingPath: options.task ? void 0 : void 0
|
|
2943
|
+
// If task provided, maybe write a temp briefing? For now standard flow.
|
|
2944
|
+
});
|
|
2945
|
+
state = taskManager.analyzeSpecState();
|
|
2946
|
+
if (state.status === "MISSING") {
|
|
2947
|
+
tui.log.error("\u274C Spec creation aborted or failed.");
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
} else {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
let keepOrchestrating = true;
|
|
2955
|
+
let burnMode = false;
|
|
2956
|
+
let contextHistory = "";
|
|
2957
|
+
tui.intro("\u{1F988} Shark Orchestrator V2");
|
|
2958
|
+
while (keepOrchestrating) {
|
|
2959
|
+
state = taskManager.analyzeSpecState();
|
|
2960
|
+
if (state.status === "COMPLETED") {
|
|
2961
|
+
tui.log.success("\u{1F389} All tasks in tech-spec.md are COMPLETED!");
|
|
2962
|
+
const choice = await tui.select({
|
|
2963
|
+
message: "What next?",
|
|
2964
|
+
options: [
|
|
2965
|
+
{ value: "exit", label: "Exit" },
|
|
2966
|
+
{ value: "new_spec", label: "New Specification (Reset)" }
|
|
2967
|
+
]
|
|
2968
|
+
});
|
|
2969
|
+
if (choice === "new_spec") {
|
|
2970
|
+
await interactiveSpecificationAgent();
|
|
2971
|
+
contextHistory = "";
|
|
2972
|
+
continue;
|
|
2973
|
+
} else {
|
|
2974
|
+
keepOrchestrating = false;
|
|
2975
|
+
break;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
const currentTask = state.nextTask;
|
|
2979
|
+
if (!currentTask) {
|
|
2980
|
+
tui.log.error("Something went wrong. Status is not completed but no next task found.");
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
tui.log.info(`
|
|
2984
|
+
\u{1F449} **NEXT TASK**: ${colors.bold(currentTask.description)}`);
|
|
2985
|
+
if (!burnMode) {
|
|
2986
|
+
const action = await tui.select({
|
|
2987
|
+
message: "Orchestration Checkpoint:",
|
|
2988
|
+
options: [
|
|
2989
|
+
{ value: "execute", label: "\u{1F680} Execute Task (Start)" },
|
|
2990
|
+
{ value: "burn", label: "\u{1F525} Burn Mode (Auto-Execute Remaining)" },
|
|
2991
|
+
{ value: "pivot", label: "\u{1F527} Pivot/Correct (Edit Spec)" },
|
|
2992
|
+
{ value: "skip", label: "\u23ED\uFE0F Skip Task (Mark Done without Executing)" },
|
|
2993
|
+
{ value: "stop", label: "\u{1F6D1} Stop Session" }
|
|
2994
|
+
]
|
|
2995
|
+
});
|
|
2996
|
+
if (action === "stop") {
|
|
2997
|
+
keepOrchestrating = false;
|
|
2998
|
+
break;
|
|
2999
|
+
} else if (action === "burn") {
|
|
3000
|
+
burnMode = true;
|
|
3001
|
+
} else if (action === "skip") {
|
|
3002
|
+
taskManager.markTaskAsDone(currentTask.id);
|
|
3003
|
+
tui.log.info("Task skipped.");
|
|
3004
|
+
continue;
|
|
3005
|
+
} else if (action === "pivot") {
|
|
3006
|
+
tui.log.info("Transferring control to Specification Agent...");
|
|
3007
|
+
await interactiveSpecificationAgent({
|
|
3008
|
+
initialContext: `User requested pivot after tasks:
|
|
3009
|
+
${contextHistory}`
|
|
3010
|
+
});
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
taskManager.markTaskInProgress(currentTask.id);
|
|
3015
|
+
tui.log.info(`\u26A1 Starting Micro-Context for Task: "${currentTask.description}"`);
|
|
3016
|
+
const result = await interactiveDeveloperAgent({
|
|
3017
|
+
taskId: currentTask.id,
|
|
3018
|
+
taskInstruction: currentTask.description,
|
|
3019
|
+
history: contextHistory,
|
|
3020
|
+
context: options.context
|
|
3021
|
+
});
|
|
3022
|
+
if (result.success) {
|
|
3023
|
+
tui.log.success(`\u2705 Task Completed: ${currentTask.description}`);
|
|
3024
|
+
taskManager.markTaskAsDone(currentTask.id);
|
|
3025
|
+
contextHistory += `
|
|
3026
|
+
[Task "${currentTask.description}" completed]: ${result.summary}`;
|
|
3027
|
+
} else {
|
|
3028
|
+
tui.log.error(`\u274C Task Failed: ${result.summary}`);
|
|
3029
|
+
burnMode = false;
|
|
3030
|
+
const recovery = await tui.select({
|
|
3031
|
+
message: "Task Failed. Recovery Action:",
|
|
3032
|
+
options: [
|
|
3033
|
+
{ value: "retry", label: "Retry (Run Agent Again)" },
|
|
3034
|
+
{ value: "pivot", label: "Pivot (Adjust Spec/Instructions)" },
|
|
3035
|
+
{ value: "ignore", label: "Ignore (Mark Done anyway)" },
|
|
3036
|
+
{ value: "stop", label: "Stop" }
|
|
3037
|
+
]
|
|
3038
|
+
});
|
|
3039
|
+
if (recovery === "stop") break;
|
|
3040
|
+
if (recovery === "ignore") taskManager.markTaskAsDone(currentTask.id);
|
|
3041
|
+
if (recovery === "pivot") {
|
|
3042
|
+
await interactiveSpecificationAgent({
|
|
3043
|
+
initialContext: `Task "${currentTask.description}" FAILED.
|
|
3044
|
+
Error: ${result.summary}
|
|
3045
|
+
History:
|
|
3046
|
+
${contextHistory}`
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
tui.outro("\u{1F988} Orchestration Finished.");
|
|
2597
3052
|
});
|
|
2598
3053
|
|
|
2599
3054
|
// src/commands/qa.ts
|
|
2600
3055
|
import { Command as Command4 } from "commander";
|
|
2601
3056
|
|
|
2602
3057
|
// src/core/agents/qa-agent.ts
|
|
2603
|
-
import
|
|
2604
|
-
import
|
|
3058
|
+
import fs9 from "fs";
|
|
3059
|
+
import path10 from "path";
|
|
2605
3060
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2606
3061
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2607
3062
|
var AGENT_TYPE5 = "qa_agent";
|
|
@@ -2670,9 +3125,9 @@ async function runQAAgent(options) {
|
|
|
2670
3125
|
}
|
|
2671
3126
|
let projectContext = "";
|
|
2672
3127
|
try {
|
|
2673
|
-
const contextPath =
|
|
2674
|
-
if (
|
|
2675
|
-
projectContext =
|
|
3128
|
+
const contextPath = path10.join(process.cwd(), "_sharkrc", "project-context.md");
|
|
3129
|
+
if (fs9.existsSync(contextPath)) {
|
|
3130
|
+
projectContext = fs9.readFileSync(contextPath, "utf-8");
|
|
2676
3131
|
tui.log.info(`\u{1F4D8} Context loaded from: _sharkrc/project-context.md`);
|
|
2677
3132
|
}
|
|
2678
3133
|
} catch (e) {
|
|
@@ -2781,11 +3236,11 @@ ${projectContext}
|
|
|
2781
3236
|
break;
|
|
2782
3237
|
case "create_file":
|
|
2783
3238
|
if (action.path && action.content) {
|
|
2784
|
-
const fullPath =
|
|
3239
|
+
const fullPath = path10.resolve(process.cwd(), action.path);
|
|
2785
3240
|
const BOM = "\uFEFF";
|
|
2786
3241
|
const contentToWrite = action.content;
|
|
2787
3242
|
const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
|
|
2788
|
-
|
|
3243
|
+
fs9.writeFileSync(fullPath, finalContent, { encoding: "utf-8" });
|
|
2789
3244
|
tui.log.success(`File created: ${action.path}`);
|
|
2790
3245
|
result = "File created successfully.";
|
|
2791
3246
|
}
|