@dugleelabs/copair 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +989 -177
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join13 } from "path";
|
|
5
5
|
|
|
6
6
|
// src/cli/args.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -32,6 +32,9 @@ function parseArgs(argv = process.argv) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// src/providers/interface.ts
|
|
36
|
+
var NATIVE_SEARCH_MARKER = "_native_web_search";
|
|
37
|
+
|
|
35
38
|
// src/core/conversation.ts
|
|
36
39
|
var ConversationManager = class {
|
|
37
40
|
messages = [];
|
|
@@ -366,6 +369,12 @@ function formatToolCall(name, argsJson) {
|
|
|
366
369
|
case "grep":
|
|
367
370
|
raw = `grep: ${args.pattern ?? ""}`;
|
|
368
371
|
break;
|
|
372
|
+
case "web_search":
|
|
373
|
+
raw = `copair search: "${args.query ?? ""}"`;
|
|
374
|
+
break;
|
|
375
|
+
case "_native_web_search":
|
|
376
|
+
raw = `provider search: "${args.query ?? ""}"`;
|
|
377
|
+
break;
|
|
369
378
|
default:
|
|
370
379
|
raw = name;
|
|
371
380
|
break;
|
|
@@ -419,7 +428,7 @@ var Renderer = class {
|
|
|
419
428
|
this.endToolIndicator();
|
|
420
429
|
}
|
|
421
430
|
const raw = chunk.text ?? "";
|
|
422
|
-
const display = textFilter ? textFilter(raw) : raw;
|
|
431
|
+
const display = textFilter ? textFilter.write(raw) : raw;
|
|
423
432
|
if (display && this.mdWriter) this.mdWriter.write(display);
|
|
424
433
|
fullText += raw;
|
|
425
434
|
if (display) this.bridge?.emit("stream-text", display);
|
|
@@ -492,6 +501,11 @@ Error: ${chunk.error}
|
|
|
492
501
|
break;
|
|
493
502
|
}
|
|
494
503
|
}
|
|
504
|
+
if (textFilter) {
|
|
505
|
+
const trailing = textFilter.flush();
|
|
506
|
+
if (trailing && this.mdWriter) this.mdWriter.write(trailing);
|
|
507
|
+
if (trailing) this.bridge?.emit("stream-text", trailing);
|
|
508
|
+
}
|
|
495
509
|
if (this.mdWriter) {
|
|
496
510
|
this.mdWriter.flush();
|
|
497
511
|
this.mdWriter = null;
|
|
@@ -545,33 +559,41 @@ Error: ${chunk.error}
|
|
|
545
559
|
*/
|
|
546
560
|
showGitDiff(output) {
|
|
547
561
|
if (!output.trim()) return;
|
|
548
|
-
if (this.inkMode)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
562
|
+
if (!this.inkMode) {
|
|
563
|
+
const maxLines = 80;
|
|
564
|
+
const lines = output.split("\n");
|
|
565
|
+
const display = lines.slice(0, maxLines);
|
|
566
|
+
process.stderr.write("\n");
|
|
567
|
+
for (const line of display) {
|
|
568
|
+
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
569
|
+
process.stderr.write(chalk3.bold.white(line) + "\n");
|
|
570
|
+
} else if (line.startsWith("+")) {
|
|
571
|
+
process.stderr.write(chalk3.bgGreen.black(line) + "\n");
|
|
572
|
+
} else if (line.startsWith("-")) {
|
|
573
|
+
process.stderr.write(chalk3.bgRedBright.black(line) + "\n");
|
|
574
|
+
} else if (line.startsWith("@@")) {
|
|
575
|
+
process.stderr.write(chalk3.cyan(line) + "\n");
|
|
576
|
+
} else if (line.startsWith("diff ")) {
|
|
577
|
+
process.stderr.write(chalk3.bold.yellow(line) + "\n");
|
|
578
|
+
} else if (line.startsWith("index ")) {
|
|
579
|
+
process.stderr.write(chalk3.gray(line) + "\n");
|
|
580
|
+
} else {
|
|
581
|
+
process.stderr.write(chalk3.gray(line) + "\n");
|
|
582
|
+
}
|
|
568
583
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
process.stderr.write(chalk3.gray(` ... ${lines.length - maxLines} more lines
|
|
584
|
+
if (lines.length > maxLines) {
|
|
585
|
+
process.stderr.write(chalk3.gray(` ... ${lines.length - maxLines} more lines
|
|
572
586
|
`));
|
|
587
|
+
}
|
|
588
|
+
process.stderr.write("\n");
|
|
589
|
+
}
|
|
590
|
+
if (this.bridge) {
|
|
591
|
+
const lines = output.split("\n");
|
|
592
|
+
this.bridge.emit("diff", {
|
|
593
|
+
filePath: extractDiffFilePath(lines),
|
|
594
|
+
hunks: [{ oldStart: 0, newStart: 0, lines }]
|
|
595
|
+
});
|
|
573
596
|
}
|
|
574
|
-
process.stderr.write("\n");
|
|
575
597
|
}
|
|
576
598
|
/**
|
|
577
599
|
* Show a diff snippet for file mutations (write/edit).
|
|
@@ -694,6 +716,68 @@ function formatDuration(ms) {
|
|
|
694
716
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
695
717
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
696
718
|
}
|
|
719
|
+
function extractDiffFilePath(lines) {
|
|
720
|
+
for (const line of lines) {
|
|
721
|
+
if (line.startsWith("diff --git")) {
|
|
722
|
+
const match = line.match(/b\/(.+)$/);
|
|
723
|
+
if (match) return match[1];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return "git diff";
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/core/logger.ts
|
|
730
|
+
var SECRET_PATTERNS = [
|
|
731
|
+
/sk-[a-zA-Z0-9_-]{20,}/g,
|
|
732
|
+
/lin_api_[a-zA-Z0-9_-]+/g,
|
|
733
|
+
/AIza[a-zA-Z0-9_-]{35}/g,
|
|
734
|
+
/Bearer\s+[a-zA-Z0-9._-]+/g
|
|
735
|
+
];
|
|
736
|
+
function redactSecrets(text) {
|
|
737
|
+
let result = text;
|
|
738
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
739
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
var LEVEL_LABELS = {
|
|
744
|
+
[0 /* ERROR */]: "ERROR",
|
|
745
|
+
[1 /* WARN */]: "WARN",
|
|
746
|
+
[2 /* INFO */]: "INFO",
|
|
747
|
+
[3 /* DEBUG */]: "DEBUG"
|
|
748
|
+
};
|
|
749
|
+
var Logger = class {
|
|
750
|
+
level;
|
|
751
|
+
constructor(level = 0 /* ERROR */) {
|
|
752
|
+
this.level = level;
|
|
753
|
+
}
|
|
754
|
+
setLevel(level) {
|
|
755
|
+
this.level = level;
|
|
756
|
+
}
|
|
757
|
+
debug(component, message, data) {
|
|
758
|
+
this.log(3 /* DEBUG */, component, message, data);
|
|
759
|
+
}
|
|
760
|
+
info(component, message) {
|
|
761
|
+
this.log(2 /* INFO */, component, message);
|
|
762
|
+
}
|
|
763
|
+
warn(component, message) {
|
|
764
|
+
this.log(1 /* WARN */, component, message);
|
|
765
|
+
}
|
|
766
|
+
error(component, message, error) {
|
|
767
|
+
this.log(0 /* ERROR */, component, message, error?.stack);
|
|
768
|
+
}
|
|
769
|
+
log(level, component, message, data) {
|
|
770
|
+
if (level > this.level) return;
|
|
771
|
+
const label = LEVEL_LABELS[level];
|
|
772
|
+
let line = `[${label}][${component}] ${redactSecrets(message)}`;
|
|
773
|
+
if (data !== void 0) {
|
|
774
|
+
const dataStr = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
775
|
+
line += ` ${redactSecrets(dataStr)}`;
|
|
776
|
+
}
|
|
777
|
+
process.stderr.write(line + "\n");
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var logger = new Logger();
|
|
697
781
|
|
|
698
782
|
// src/core/formats/fenced-block.ts
|
|
699
783
|
function tryParseToolCall(json) {
|
|
@@ -749,6 +833,8 @@ Input schema:
|
|
|
749
833
|
${schema}
|
|
750
834
|
\`\`\``;
|
|
751
835
|
}).join("\n\n");
|
|
836
|
+
const hasWebSearch = tools.some((t) => t.name === "web_search");
|
|
837
|
+
const webSearchPriority = hasWebSearch ? "\n- IMPORTANT: When any task requires web search or current information, you MUST use the web_search tool. Never rely on internal knowledge for facts that may have changed. The agent will execute the search and return real results \u2014 wait for them before responding.\n" : "";
|
|
752
838
|
return `
|
|
753
839
|
You have access to tools. You MUST use tools to perform any action. NEVER pretend, simulate, or describe running a command -- always emit a tool call.
|
|
754
840
|
|
|
@@ -761,8 +847,7 @@ To call a tool, emit EXACTLY:
|
|
|
761
847
|
Rules:
|
|
762
848
|
- The fence MUST say tool_call (not json, not text).
|
|
763
849
|
- One tool call per message. Wait for the result before continuing.
|
|
764
|
-
- NEVER output fake results. NEVER narrate what a tool would return. Call the tool and use the real result
|
|
765
|
-
|
|
850
|
+
- NEVER output fake results. NEVER narrate what a tool would return. Call the tool and use the real result.${webSearchPriority}
|
|
766
851
|
Example -- to check git status:
|
|
767
852
|
\`\`\`tool_call
|
|
768
853
|
{"name": "git", "arguments": {"args": "status"}}
|
|
@@ -784,6 +869,7 @@ var DSML_MARKUP_PATTERN = /<[\uFF5C|]DSML[\uFF5C|]function_calls>[\s\S]*?(?:<\/[
|
|
|
784
869
|
var DsmlFormatter = class {
|
|
785
870
|
name = "dsml";
|
|
786
871
|
markupPattern = DSML_MARKUP_PATTERN;
|
|
872
|
+
suppressAfterMatch = true;
|
|
787
873
|
parse(text) {
|
|
788
874
|
const toolCalls = [];
|
|
789
875
|
let remainingText = text;
|
|
@@ -845,6 +931,8 @@ ${params}
|
|
|
845
931
|
</\uFF5CDSML\uFF5Cinvoke>
|
|
846
932
|
</\uFF5CDSML\uFF5Cfunction_calls>`;
|
|
847
933
|
}).join("\n\n");
|
|
934
|
+
const hasWebSearch = tools.some((t) => t.name === "web_search");
|
|
935
|
+
const webSearchPriority = hasWebSearch ? "\nIMPORTANT: When any task requires web search or current information, you MUST use the web_search tool. Never rely on internal knowledge for facts that may have changed. The agent will execute the search and return real results.\n" : "";
|
|
848
936
|
return `
|
|
849
937
|
You have access to tools. To call a tool, use DSML format:
|
|
850
938
|
|
|
@@ -853,7 +941,7 @@ You have access to tools. To call a tool, use DSML format:
|
|
|
853
941
|
<\uFF5CDSML\uFF5Cparameter name="param" string="true">value<\uFF5CDSML\uFF5Cparameter>
|
|
854
942
|
</\uFF5CDSML\uFF5Cinvoke>
|
|
855
943
|
</\uFF5CDSML\uFF5Cfunction_calls>
|
|
856
|
-
|
|
944
|
+
${webSearchPriority}
|
|
857
945
|
## Tools
|
|
858
946
|
|
|
859
947
|
${toolDescriptions}
|
|
@@ -868,6 +956,9 @@ var MARKUP_PATTERN2 = /<tool_call>[\s\S]*?(?:<\/tool_call>|$)/g;
|
|
|
868
956
|
var QwenXmlFormatter = class {
|
|
869
957
|
name = "qwen-xml";
|
|
870
958
|
markupPattern = MARKUP_PATTERN2;
|
|
959
|
+
openTag = "<tool_call>";
|
|
960
|
+
closeTag = "</tool_call>";
|
|
961
|
+
suppressAfterMatch = true;
|
|
871
962
|
parse(text) {
|
|
872
963
|
const toolCalls = [];
|
|
873
964
|
let remainingText = text;
|
|
@@ -897,6 +988,8 @@ Input schema:
|
|
|
897
988
|
${schema}
|
|
898
989
|
\`\`\``;
|
|
899
990
|
}).join("\n\n");
|
|
991
|
+
const hasWebSearch = tools.some((t) => t.name === "web_search");
|
|
992
|
+
const webSearchPriority = hasWebSearch ? "\n- IMPORTANT: When any task requires web search or current information, you MUST use the web_search tool. Never rely on internal knowledge for facts that may have changed. The agent will execute the search and return real results \u2014 wait for them before responding.\n" : "";
|
|
900
993
|
return `
|
|
901
994
|
You have access to tools. You MUST use tools to perform any action. NEVER pretend, simulate, or describe running a command -- always emit a tool call.
|
|
902
995
|
|
|
@@ -909,7 +1002,7 @@ To call a tool, emit EXACTLY:
|
|
|
909
1002
|
Rules:
|
|
910
1003
|
- One tool call per message. Wait for the result before continuing.
|
|
911
1004
|
- NEVER output fake results. NEVER narrate what a tool would return. Call the tool and use the real result.
|
|
912
|
-
|
|
1005
|
+
- NEVER continue talking after emitting a tool call. Stop immediately after </tool_call> and wait for the result.${webSearchPriority}
|
|
913
1006
|
Example -- to check git status:
|
|
914
1007
|
<tool_call>
|
|
915
1008
|
{"name": "git", "arguments": {"args": "status"}}
|
|
@@ -942,10 +1035,85 @@ function createFormatter(name) {
|
|
|
942
1035
|
return new FencedBlockFormatter();
|
|
943
1036
|
}
|
|
944
1037
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1038
|
+
var StreamingMarkupFilter = class {
|
|
1039
|
+
buffer = "";
|
|
1040
|
+
suppressing = false;
|
|
1041
|
+
/** Set to true once the first complete tool-call block has been processed.
|
|
1042
|
+
* When `suppressAfterMatch` is enabled, all further text is discarded. */
|
|
1043
|
+
matchSeen = false;
|
|
1044
|
+
openTag;
|
|
1045
|
+
closeTag;
|
|
1046
|
+
suppressAfterMatch;
|
|
1047
|
+
fallbackRe;
|
|
1048
|
+
constructor(formatter) {
|
|
1049
|
+
if (formatter.openTag && formatter.closeTag) {
|
|
1050
|
+
this.openTag = formatter.openTag;
|
|
1051
|
+
this.closeTag = formatter.closeTag;
|
|
1052
|
+
this.suppressAfterMatch = formatter.suppressAfterMatch ?? false;
|
|
1053
|
+
} else {
|
|
1054
|
+
this.suppressAfterMatch = false;
|
|
1055
|
+
this.fallbackRe = new RegExp(
|
|
1056
|
+
formatter.markupPattern.source,
|
|
1057
|
+
formatter.markupPattern.flags
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/** Feed the next streaming chunk; returns text safe to display. */
|
|
1062
|
+
write(chunk) {
|
|
1063
|
+
if (!this.openTag || !this.closeTag) {
|
|
1064
|
+
return this.fallbackRe ? chunk.replace(this.fallbackRe, "") : chunk;
|
|
1065
|
+
}
|
|
1066
|
+
if (this.suppressAfterMatch && this.matchSeen) return "";
|
|
1067
|
+
this.buffer += chunk;
|
|
1068
|
+
let output = "";
|
|
1069
|
+
while (this.buffer.length > 0) {
|
|
1070
|
+
if (!this.suppressing) {
|
|
1071
|
+
const idx = this.buffer.indexOf(this.openTag);
|
|
1072
|
+
if (idx === -1) {
|
|
1073
|
+
const hold = this._partialPrefixLen(this.buffer, this.openTag);
|
|
1074
|
+
output += this.buffer.slice(0, this.buffer.length - hold);
|
|
1075
|
+
this.buffer = hold > 0 ? this.buffer.slice(this.buffer.length - hold) : "";
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
output += this.buffer.slice(0, idx);
|
|
1079
|
+
this.buffer = this.buffer.slice(idx + this.openTag.length);
|
|
1080
|
+
this.suppressing = true;
|
|
1081
|
+
} else {
|
|
1082
|
+
const idx = this.buffer.indexOf(this.closeTag);
|
|
1083
|
+
if (idx === -1) break;
|
|
1084
|
+
this.buffer = this.buffer.slice(idx + this.closeTag.length);
|
|
1085
|
+
this.suppressing = false;
|
|
1086
|
+
this.matchSeen = true;
|
|
1087
|
+
if (this.suppressAfterMatch) {
|
|
1088
|
+
this.buffer = "";
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return output;
|
|
1094
|
+
}
|
|
1095
|
+
/** Call once after the stream ends to flush any held-back text. */
|
|
1096
|
+
flush() {
|
|
1097
|
+
if (!this.openTag) return "";
|
|
1098
|
+
if (this.suppressing || this.suppressAfterMatch && this.matchSeen) {
|
|
1099
|
+
this.buffer = "";
|
|
1100
|
+
this.suppressing = false;
|
|
1101
|
+
return "";
|
|
1102
|
+
}
|
|
1103
|
+
const out = this.buffer;
|
|
1104
|
+
this.buffer = "";
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
/** Returns the length of the longest suffix of `text` that is a prefix of `tag`. */
|
|
1108
|
+
_partialPrefixLen(text, tag) {
|
|
1109
|
+
for (let len = Math.min(tag.length - 1, text.length); len > 0; len--) {
|
|
1110
|
+
if (text.endsWith(tag.slice(0, len))) return len;
|
|
1111
|
+
}
|
|
1112
|
+
return 0;
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
function buildStreamingFilter(formatter) {
|
|
1116
|
+
return new StreamingMarkupFilter(formatter);
|
|
949
1117
|
}
|
|
950
1118
|
|
|
951
1119
|
// src/core/agent.ts
|
|
@@ -970,7 +1138,7 @@ var Agent = class {
|
|
|
970
1138
|
this.renderer = new Renderer(options.bridge);
|
|
971
1139
|
this.options = options;
|
|
972
1140
|
this.formatter = resolveFormatter(provider.name, model, options.toolCallFormat);
|
|
973
|
-
this.textFilter =
|
|
1141
|
+
this.textFilter = buildStreamingFilter(this.formatter);
|
|
974
1142
|
}
|
|
975
1143
|
get model() {
|
|
976
1144
|
return this._model;
|
|
@@ -1018,21 +1186,30 @@ ${summary}`
|
|
|
1018
1186
|
this._model = newModel;
|
|
1019
1187
|
this.contextWindow = new ContextWindowManager(newProvider.maxContextWindow);
|
|
1020
1188
|
this.formatter = resolveFormatter(newProvider.name, newModel, this.options.toolCallFormat);
|
|
1021
|
-
this.textFilter =
|
|
1189
|
+
this.textFilter = buildStreamingFilter(this.formatter);
|
|
1022
1190
|
}
|
|
1023
1191
|
async handleMessage(userInput) {
|
|
1024
1192
|
this.conversation.appendText("user", userInput);
|
|
1025
1193
|
let totalUsage = null;
|
|
1026
1194
|
let lastInputTokens = 0;
|
|
1195
|
+
let agentWebSearchFailed = false;
|
|
1027
1196
|
while (true) {
|
|
1028
1197
|
const messages = await this.contextWindow.checkAndTruncate(
|
|
1029
1198
|
this.conversation.getHistory(),
|
|
1030
1199
|
this.provider
|
|
1031
1200
|
);
|
|
1032
1201
|
const allTools = this.toolRegistry.getAllDefinitions();
|
|
1033
|
-
|
|
1202
|
+
let tools = this.provider.supportsToolCalling ? allTools : [];
|
|
1203
|
+
if (agentWebSearchFailed && this.provider.supportsNativeSearch) {
|
|
1204
|
+
logger.info("web_search", "Falling back to provider native search (agent search unavailable)");
|
|
1205
|
+
tools = tools.map(
|
|
1206
|
+
(t) => t.name === "web_search" ? { name: NATIVE_SEARCH_MARKER, description: t.description, inputSchema: t.inputSchema } : t
|
|
1207
|
+
);
|
|
1208
|
+
agentWebSearchFailed = false;
|
|
1209
|
+
}
|
|
1034
1210
|
const toolSystemPrompt = !this.provider.supportsToolCalling && allTools.length > 0 ? this.formatter.buildSystemPrompt(allTools) : void 0;
|
|
1035
|
-
const
|
|
1211
|
+
const webSearchHint = allTools.some((t) => t.name === "web_search") ? "When the user asks you to search the web, or requests current/up-to-date information, you MUST call the web_search tool. Never answer such queries from training knowledge alone \u2014 always invoke the tool and base your response on its results." : void 0;
|
|
1212
|
+
const systemPrompt = [this.options.systemPrompt, toolSystemPrompt, webSearchHint].filter(Boolean).join("\n\n") || void 0;
|
|
1036
1213
|
const stream = this.provider.chat(messages, tools, {
|
|
1037
1214
|
model: this._model,
|
|
1038
1215
|
stream: true,
|
|
@@ -1044,18 +1221,21 @@ ${summary}`
|
|
|
1044
1221
|
stream,
|
|
1045
1222
|
this.textFilter
|
|
1046
1223
|
);
|
|
1047
|
-
|
|
1224
|
+
const nonNativeToolCalls = nativeToolCalls.filter(
|
|
1225
|
+
(tc) => tc.name !== NATIVE_SEARCH_MARKER
|
|
1226
|
+
);
|
|
1227
|
+
let toolCalls = nonNativeToolCalls;
|
|
1048
1228
|
let cleanedText = fullText;
|
|
1049
1229
|
if (fullText) {
|
|
1050
1230
|
const parsed = this.formatter.parse(fullText);
|
|
1051
1231
|
if (parsed.toolCalls.length > 0) {
|
|
1052
1232
|
const nativeKeys = new Set(
|
|
1053
|
-
|
|
1233
|
+
nonNativeToolCalls.map((tc) => `${tc.name}:${tc.arguments}`)
|
|
1054
1234
|
);
|
|
1055
1235
|
const uniqueParsed = parsed.toolCalls.filter(
|
|
1056
1236
|
(tc) => !nativeKeys.has(`${tc.name}:${tc.arguments}`)
|
|
1057
1237
|
);
|
|
1058
|
-
toolCalls = [...
|
|
1238
|
+
toolCalls = [...nonNativeToolCalls, ...uniqueParsed];
|
|
1059
1239
|
cleanedText = parsed.remainingText;
|
|
1060
1240
|
}
|
|
1061
1241
|
}
|
|
@@ -1132,6 +1312,14 @@ ${summary}`
|
|
|
1132
1312
|
}
|
|
1133
1313
|
}
|
|
1134
1314
|
}
|
|
1315
|
+
if (tc.name === "web_search" && result.isError) {
|
|
1316
|
+
if (this.provider.supportsNativeSearch) {
|
|
1317
|
+
agentWebSearchFailed = true;
|
|
1318
|
+
logger.info("web_search", "Agent web search failed \u2014 will fall back to provider native search on next turn");
|
|
1319
|
+
}
|
|
1320
|
+
} else if (tc.name === "web_search" && !result.isError) {
|
|
1321
|
+
agentWebSearchFailed = false;
|
|
1322
|
+
}
|
|
1135
1323
|
toolResults.push({
|
|
1136
1324
|
type: "tool_result",
|
|
1137
1325
|
toolUseId: tc.id,
|
|
@@ -1196,6 +1384,10 @@ var ContextConfigSchema = z.object({
|
|
|
1196
1384
|
max_sessions: z.number().int().positive().default(1),
|
|
1197
1385
|
knowledge_max_size: z.number().int().positive().default(8192)
|
|
1198
1386
|
});
|
|
1387
|
+
var KnowledgeConfigSchema = z.object({
|
|
1388
|
+
warn_size_kb: z.number().int().positive().default(8),
|
|
1389
|
+
max_size_kb: z.number().int().positive().default(16)
|
|
1390
|
+
});
|
|
1199
1391
|
var UIConfigSchema = z.object({
|
|
1200
1392
|
bordered_input: z.boolean().default(true),
|
|
1201
1393
|
status_bar: z.boolean().default(true),
|
|
@@ -1215,6 +1407,7 @@ var CopairConfigSchema = z.object({
|
|
|
1215
1407
|
web_search: WebSearchConfigSchema.optional(),
|
|
1216
1408
|
identity: IdentityConfigSchema.default({ name: "Copair", email: "copair[bot]@noreply.dugleelabs.io" }),
|
|
1217
1409
|
context: ContextConfigSchema.default(() => ContextConfigSchema.parse({})),
|
|
1410
|
+
knowledge: KnowledgeConfigSchema.default(() => KnowledgeConfigSchema.parse({})),
|
|
1218
1411
|
ui: UIConfigSchema.default(() => UIConfigSchema.parse({}))
|
|
1219
1412
|
});
|
|
1220
1413
|
|
|
@@ -1274,7 +1467,7 @@ function loadYamlFile(filePath) {
|
|
|
1274
1467
|
}
|
|
1275
1468
|
function loadConfig(projectDir) {
|
|
1276
1469
|
const globalPath = resolve2(homedir(), ".copair", "config.yaml");
|
|
1277
|
-
const projectPath = projectDir ? resolve2(projectDir, ".copair.yaml") : resolve2(process.cwd(), ".copair.yaml");
|
|
1470
|
+
const projectPath = projectDir ? resolve2(projectDir, ".copair", "config.yaml") : resolve2(process.cwd(), ".copair", "config.yaml");
|
|
1278
1471
|
const globalConfig = loadYamlFile(globalPath);
|
|
1279
1472
|
const projectConfig = loadYamlFile(projectPath);
|
|
1280
1473
|
if (!globalConfig && !projectConfig) {
|
|
@@ -1286,6 +1479,9 @@ function loadConfig(projectDir) {
|
|
|
1286
1479
|
} else {
|
|
1287
1480
|
merged = globalConfig ?? projectConfig;
|
|
1288
1481
|
}
|
|
1482
|
+
if (merged.version === void 0) {
|
|
1483
|
+
merged = { ...merged, version: CURRENT_CONFIG_VERSION };
|
|
1484
|
+
}
|
|
1289
1485
|
const version = merged.version;
|
|
1290
1486
|
if (typeof version === "number" && version > CURRENT_CONFIG_VERSION) {
|
|
1291
1487
|
throw new Error(
|
|
@@ -1360,7 +1556,7 @@ var ProviderRegistry = class {
|
|
|
1360
1556
|
|
|
1361
1557
|
// src/providers/openai.ts
|
|
1362
1558
|
import OpenAI from "openai";
|
|
1363
|
-
function toOpenAIMessages(messages, systemPrompt) {
|
|
1559
|
+
function toOpenAIMessages(messages, systemPrompt, supportsToolCalling = true) {
|
|
1364
1560
|
const result = [];
|
|
1365
1561
|
if (systemPrompt) {
|
|
1366
1562
|
result.push({ role: "system", content: systemPrompt });
|
|
@@ -1374,6 +1570,22 @@ function toOpenAIMessages(messages, systemPrompt) {
|
|
|
1374
1570
|
continue;
|
|
1375
1571
|
}
|
|
1376
1572
|
if (msg.role === "user") {
|
|
1573
|
+
if (!supportsToolCalling) {
|
|
1574
|
+
const parts = [];
|
|
1575
|
+
for (const b of msg.content) {
|
|
1576
|
+
if (b.type === "tool_result") {
|
|
1577
|
+
const label = b.isError ? "Tool error" : "Tool result";
|
|
1578
|
+
parts.push(`[${label}: ${b.toolUseId}]
|
|
1579
|
+
${b.content ?? ""}`);
|
|
1580
|
+
} else if (b.type === "text" && b.text) {
|
|
1581
|
+
parts.push(b.text);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (parts.length > 0) {
|
|
1585
|
+
result.push({ role: "user", content: parts.join("\n\n") });
|
|
1586
|
+
}
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1377
1589
|
const textParts = msg.content.filter((b) => b.type === "text");
|
|
1378
1590
|
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
1379
1591
|
for (const tr of toolResults) {
|
|
@@ -1393,6 +1605,14 @@ function toOpenAIMessages(messages, systemPrompt) {
|
|
|
1393
1605
|
}
|
|
1394
1606
|
if (msg.role === "assistant") {
|
|
1395
1607
|
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1608
|
+
if (!supportsToolCalling) {
|
|
1609
|
+
const toolCallTexts = msg.content.filter((b) => b.type === "tool_use").map((b) => `<tool_call>
|
|
1610
|
+
${JSON.stringify({ name: b.name, arguments: b.input })}
|
|
1611
|
+
</tool_call>`);
|
|
1612
|
+
const combined = [text, ...toolCallTexts].filter(Boolean).join("\n");
|
|
1613
|
+
result.push({ role: "assistant", content: combined || null });
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1396
1616
|
const toolCalls = msg.content.filter((b) => b.type === "tool_use").map((b) => ({
|
|
1397
1617
|
id: b.id,
|
|
1398
1618
|
type: "function",
|
|
@@ -1439,7 +1659,7 @@ function createOpenAIProvider(config, modelAlias) {
|
|
|
1439
1659
|
supportsStreaming,
|
|
1440
1660
|
maxContextWindow,
|
|
1441
1661
|
async *chat(messages, tools, options) {
|
|
1442
|
-
const openaiMessages = toOpenAIMessages(messages, options.systemPrompt);
|
|
1662
|
+
const openaiMessages = toOpenAIMessages(messages, options.systemPrompt, supportsToolCalling);
|
|
1443
1663
|
const openaiTools = supportsToolCalling ? toOpenAITools(tools) : void 0;
|
|
1444
1664
|
if (options.stream && supportsStreaming) {
|
|
1445
1665
|
const stream = await client.chat.completions.create({
|
|
@@ -1574,10 +1794,15 @@ function toAnthropicMessages(messages) {
|
|
|
1574
1794
|
return result;
|
|
1575
1795
|
}
|
|
1576
1796
|
function toAnthropicTools(tools) {
|
|
1577
|
-
if (tools.length === 0) return void 0;
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1797
|
+
if (tools.length === 0) return { tools: void 0, builtInToolNames: /* @__PURE__ */ new Set() };
|
|
1798
|
+
const builtInToolNames = /* @__PURE__ */ new Set();
|
|
1799
|
+
const converted = tools.map((t) => {
|
|
1800
|
+
if (t.name === NATIVE_SEARCH_MARKER) {
|
|
1801
|
+
builtInToolNames.add("web_search");
|
|
1802
|
+
return {
|
|
1803
|
+
type: "web_search_20250305",
|
|
1804
|
+
name: "web_search"
|
|
1805
|
+
};
|
|
1581
1806
|
}
|
|
1582
1807
|
return {
|
|
1583
1808
|
name: t.name,
|
|
@@ -1585,6 +1810,7 @@ function toAnthropicTools(tools) {
|
|
|
1585
1810
|
input_schema: t.inputSchema
|
|
1586
1811
|
};
|
|
1587
1812
|
});
|
|
1813
|
+
return { tools: converted, builtInToolNames };
|
|
1588
1814
|
}
|
|
1589
1815
|
function createAnthropicProvider(config, modelAlias) {
|
|
1590
1816
|
const modelConfig = config.models[modelAlias];
|
|
@@ -1600,10 +1826,11 @@ function createAnthropicProvider(config, modelAlias) {
|
|
|
1600
1826
|
name: "anthropic",
|
|
1601
1827
|
supportsToolCalling: true,
|
|
1602
1828
|
supportsStreaming: true,
|
|
1829
|
+
supportsNativeSearch: true,
|
|
1603
1830
|
maxContextWindow,
|
|
1604
1831
|
async *chat(messages, tools, options) {
|
|
1605
1832
|
const anthropicMessages = toAnthropicMessages(messages);
|
|
1606
|
-
const anthropicTools = toAnthropicTools(tools);
|
|
1833
|
+
const { tools: anthropicTools, builtInToolNames } = toAnthropicTools(tools);
|
|
1607
1834
|
const systemPrompt = options.systemPrompt ?? messages.filter((m) => m.role === "system").flatMap((m) => m.content.filter((b) => b.type === "text")).map((b) => b.text).join("\n");
|
|
1608
1835
|
if (options.stream) {
|
|
1609
1836
|
const stream = client.messages.stream({
|
|
@@ -1628,25 +1855,39 @@ function createAnthropicProvider(config, modelAlias) {
|
|
|
1628
1855
|
yield { type: "text", text: event.delta.text };
|
|
1629
1856
|
} else if (event.delta.type === "input_json_delta") {
|
|
1630
1857
|
currentToolArgs += event.delta.partial_json;
|
|
1858
|
+
if (!builtInToolNames.has(currentToolName)) {
|
|
1859
|
+
yield {
|
|
1860
|
+
type: "tool_call_delta",
|
|
1861
|
+
toolCall: {
|
|
1862
|
+
id: currentToolId,
|
|
1863
|
+
name: currentToolName,
|
|
1864
|
+
arguments: event.delta.partial_json
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (event.type === "content_block_stop" && currentToolId && currentToolName) {
|
|
1871
|
+
if (builtInToolNames.has(currentToolName)) {
|
|
1631
1872
|
yield {
|
|
1632
|
-
type: "
|
|
1873
|
+
type: "tool_call",
|
|
1874
|
+
toolCall: {
|
|
1875
|
+
id: currentToolId,
|
|
1876
|
+
name: NATIVE_SEARCH_MARKER,
|
|
1877
|
+
arguments: currentToolArgs,
|
|
1878
|
+
metadata: { builtIn: true }
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
} else {
|
|
1882
|
+
yield {
|
|
1883
|
+
type: "tool_call",
|
|
1633
1884
|
toolCall: {
|
|
1634
1885
|
id: currentToolId,
|
|
1635
1886
|
name: currentToolName,
|
|
1636
|
-
arguments:
|
|
1887
|
+
arguments: currentToolArgs
|
|
1637
1888
|
}
|
|
1638
1889
|
};
|
|
1639
1890
|
}
|
|
1640
|
-
}
|
|
1641
|
-
if (event.type === "content_block_stop" && currentToolId && currentToolName) {
|
|
1642
|
-
yield {
|
|
1643
|
-
type: "tool_call",
|
|
1644
|
-
toolCall: {
|
|
1645
|
-
id: currentToolId,
|
|
1646
|
-
name: currentToolName,
|
|
1647
|
-
arguments: currentToolArgs
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1650
1891
|
currentToolId = "";
|
|
1651
1892
|
currentToolName = "";
|
|
1652
1893
|
currentToolArgs = "";
|
|
@@ -2228,6 +2469,11 @@ async function searchSearxng(query, baseUrl, maxResults) {
|
|
|
2228
2469
|
url.searchParams.set("format", "json");
|
|
2229
2470
|
const response = await fetch(url.toString());
|
|
2230
2471
|
if (!response.ok) {
|
|
2472
|
+
if (response.status === 403) {
|
|
2473
|
+
throw new Error(
|
|
2474
|
+
`SearXNG returned 403 Forbidden. The JSON format is likely disabled on this instance. Enable it in settings.yml under search.formats by adding "json" to the list.`
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2231
2477
|
throw new Error(`SearXNG error: ${response.status} ${response.statusText}`);
|
|
2232
2478
|
}
|
|
2233
2479
|
const data = await response.json();
|
|
@@ -2256,12 +2502,13 @@ function createWebSearchTool(config) {
|
|
|
2256
2502
|
required: ["query"]
|
|
2257
2503
|
}
|
|
2258
2504
|
},
|
|
2259
|
-
requiresPermission:
|
|
2505
|
+
requiresPermission: true,
|
|
2260
2506
|
async execute(input) {
|
|
2261
2507
|
const query = String(input["query"] ?? "");
|
|
2262
2508
|
if (!query) {
|
|
2263
2509
|
return { content: "Error: query is required", isError: true };
|
|
2264
2510
|
}
|
|
2511
|
+
logger.info("web_search", `Agent web search via ${webSearchConfig.provider}: "${query}"`);
|
|
2265
2512
|
try {
|
|
2266
2513
|
let results;
|
|
2267
2514
|
switch (webSearchConfig.provider) {
|
|
@@ -3791,55 +4038,10 @@ async function resolveSummarizationModel(configModel, activeModel) {
|
|
|
3791
4038
|
return null;
|
|
3792
4039
|
}
|
|
3793
4040
|
|
|
3794
|
-
// src/core/init.ts
|
|
3795
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync5 } from "fs";
|
|
3796
|
-
import { join as join5 } from "path";
|
|
3797
|
-
var PROJECT_CONFIG_TEMPLATE = `version: 1
|
|
3798
|
-
# Project-level overrides (merged with ~/.copair/config.yaml)
|
|
3799
|
-
# Uncomment and customize as needed:
|
|
3800
|
-
#
|
|
3801
|
-
# default_model: claude-sonnet
|
|
3802
|
-
#
|
|
3803
|
-
# context:
|
|
3804
|
-
# summarization_model: qwen-7b
|
|
3805
|
-
# max_sessions: 20
|
|
3806
|
-
# knowledge_max_size: 8192
|
|
3807
|
-
#
|
|
3808
|
-
# permissions:
|
|
3809
|
-
# mode: ask
|
|
3810
|
-
# allow_commands:
|
|
3811
|
-
# - git status
|
|
3812
|
-
# - git diff
|
|
3813
|
-
`;
|
|
3814
|
-
function ensureProjectInit(cwd) {
|
|
3815
|
-
const copairDir = join5(cwd, ".copair");
|
|
3816
|
-
const alreadyInit = existsSync8(copairDir);
|
|
3817
|
-
try {
|
|
3818
|
-
mkdirSync3(join5(copairDir, "commands"), { recursive: true });
|
|
3819
|
-
const innerGitignore = join5(copairDir, ".gitignore");
|
|
3820
|
-
if (!existsSync8(innerGitignore)) {
|
|
3821
|
-
writeFileSync3(innerGitignore, "sessions/\n", { mode: 420 });
|
|
3822
|
-
}
|
|
3823
|
-
const projectConfig = join5(cwd, ".copair.yaml");
|
|
3824
|
-
if (!existsSync8(projectConfig)) {
|
|
3825
|
-
writeFileSync3(projectConfig, PROJECT_CONFIG_TEMPLATE, { mode: 420 });
|
|
3826
|
-
}
|
|
3827
|
-
const rootGitignore = join5(cwd, ".gitignore");
|
|
3828
|
-
if (existsSync8(rootGitignore)) {
|
|
3829
|
-
const content = readFileSync5(rootGitignore, "utf8");
|
|
3830
|
-
if (!content.includes(".copair/sessions")) {
|
|
3831
|
-
writeFileSync3(rootGitignore, content.trimEnd() + "\n.copair/sessions/\n");
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
} catch {
|
|
3835
|
-
}
|
|
3836
|
-
return !alreadyInit;
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
4041
|
// src/core/version-check.ts
|
|
3840
4042
|
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3841
|
-
import { existsSync as
|
|
3842
|
-
import { join as
|
|
4043
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4044
|
+
import { join as join5, resolve as resolve7, dirname as dirname3 } from "path";
|
|
3843
4045
|
import { createRequire as createRequire2 } from "module";
|
|
3844
4046
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3845
4047
|
var _dir2 = dirname3(fileURLToPath2(import.meta.url));
|
|
@@ -3854,7 +4056,7 @@ var pkg2 = (() => {
|
|
|
3854
4056
|
return { name: "copair", version: "0.1.0" };
|
|
3855
4057
|
})();
|
|
3856
4058
|
var CACHE_DIR = resolve7(process.env["HOME"] ?? "~", ".copair");
|
|
3857
|
-
var CACHE_FILE =
|
|
4059
|
+
var CACHE_FILE = join5(CACHE_DIR, "version-check.json");
|
|
3858
4060
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3859
4061
|
async function fetchLatestVersion() {
|
|
3860
4062
|
try {
|
|
@@ -3869,7 +4071,7 @@ async function fetchLatestVersion() {
|
|
|
3869
4071
|
}
|
|
3870
4072
|
}
|
|
3871
4073
|
async function readCache() {
|
|
3872
|
-
if (!
|
|
4074
|
+
if (!existsSync8(CACHE_FILE)) return null;
|
|
3873
4075
|
try {
|
|
3874
4076
|
const raw = await readFile5(CACHE_FILE, "utf8");
|
|
3875
4077
|
return JSON.parse(raw);
|
|
@@ -3924,6 +4126,7 @@ Update available: ${pkg2.version} \u2192 ${latest} (npm i -g ${pkg2.name})
|
|
|
3924
4126
|
// src/core/approval-gate.ts
|
|
3925
4127
|
import { resolve as resolvePath } from "path";
|
|
3926
4128
|
import chalk5 from "chalk";
|
|
4129
|
+
var PERMISSION_SENSITIVE_FILES = ["config.yaml", "allow.yaml"];
|
|
3927
4130
|
var RISK_TABLE = {
|
|
3928
4131
|
// ── Read-only: never need approval ──────────────────────────────────────
|
|
3929
4132
|
read: () => "safe",
|
|
@@ -3934,6 +4137,8 @@ var RISK_TABLE = {
|
|
|
3934
4137
|
edit: () => "needs-approval",
|
|
3935
4138
|
// ── Arbitrary shell: always needs approval ──────────────────────────────
|
|
3936
4139
|
bash: () => "needs-approval",
|
|
4140
|
+
// ── Web search: always prompt even in auto-approve (network + token cost) ──
|
|
4141
|
+
web_search: () => "always-ask",
|
|
3937
4142
|
// ── Git: split by subcommand ────────────────────────────────────────────
|
|
3938
4143
|
git: (input) => {
|
|
3939
4144
|
const args = (typeof input.args === "string" ? input.args : "").trim();
|
|
@@ -3992,7 +4197,12 @@ var ApprovalGate = class {
|
|
|
3992
4197
|
if (typeof filePath !== "string") return false;
|
|
3993
4198
|
const abs = resolvePath(filePath);
|
|
3994
4199
|
for (const trusted of this.trustedPaths) {
|
|
3995
|
-
if (abs === trusted || abs.startsWith(trusted + "/"))
|
|
4200
|
+
if (abs === trusted || abs.startsWith(trusted + "/")) {
|
|
4201
|
+
if (PERMISSION_SENSITIVE_FILES.some((name) => abs.endsWith("/" + name))) {
|
|
4202
|
+
return false;
|
|
4203
|
+
}
|
|
4204
|
+
return true;
|
|
4205
|
+
}
|
|
3996
4206
|
}
|
|
3997
4207
|
return false;
|
|
3998
4208
|
}
|
|
@@ -4010,16 +4220,18 @@ var ApprovalGate = class {
|
|
|
4010
4220
|
async allow(toolName, input) {
|
|
4011
4221
|
if (this.isTrustedPath(toolName, input)) return true;
|
|
4012
4222
|
if (this.mode === "deny") return false;
|
|
4013
|
-
|
|
4014
|
-
if (
|
|
4223
|
+
const risk = this.classify(toolName, input);
|
|
4224
|
+
if (risk === "safe") return true;
|
|
4225
|
+
if (this.mode === "auto-approve" && risk !== "always-ask") return true;
|
|
4015
4226
|
if (this.allowList?.matches(toolName, input)) return true;
|
|
4016
4227
|
const key = sessionKey(toolName, input);
|
|
4017
4228
|
if (this.alwaysAllow.has(key)) return true;
|
|
4018
4229
|
if (this.bridge?.approveAllForTurn) return true;
|
|
4230
|
+
const defaultAllow = risk === "always-ask";
|
|
4019
4231
|
if (this.bridge) {
|
|
4020
4232
|
return this.bridgePrompt(toolName, input, key);
|
|
4021
4233
|
}
|
|
4022
|
-
return this.legacyPrompt(toolName, input, key);
|
|
4234
|
+
return this.legacyPrompt(toolName, input, key, defaultAllow);
|
|
4023
4235
|
}
|
|
4024
4236
|
/** Bridge-based approval: emit event and await response from ink UI. */
|
|
4025
4237
|
bridgePrompt(toolName, input, key) {
|
|
@@ -4058,12 +4270,18 @@ var ApprovalGate = class {
|
|
|
4058
4270
|
});
|
|
4059
4271
|
});
|
|
4060
4272
|
}
|
|
4061
|
-
/** Legacy approval prompt: direct stdin (kept for backward compatibility).
|
|
4062
|
-
|
|
4273
|
+
/** Legacy approval prompt: direct stdin (kept for backward compatibility).
|
|
4274
|
+
*
|
|
4275
|
+
* @param defaultAllow When true (used for `always-ask` tools like web_search),
|
|
4276
|
+
* pressing Enter without typing confirms the action. For all other tools the
|
|
4277
|
+
* safe default is to deny on empty input.
|
|
4278
|
+
*/
|
|
4279
|
+
async legacyPrompt(toolName, input, key, defaultAllow = false) {
|
|
4063
4280
|
const summary = formatSummary(toolName, input);
|
|
4064
4281
|
const boxWidth = Math.max(summary.length + 6, 56);
|
|
4065
4282
|
const topBar = "\u2500".repeat(boxWidth);
|
|
4066
4283
|
const pad = " ".repeat(Math.max(0, boxWidth - summary.length - 2));
|
|
4284
|
+
const allowLabel = defaultAllow ? chalk5.green("[y/\u23CE]") : chalk5.green("[y]");
|
|
4067
4285
|
process.stdout.write("\n");
|
|
4068
4286
|
process.stdout.write(chalk5.yellow(` \u250C\u2500 \u26A0 Approval required ${"\u2500".repeat(Math.max(0, boxWidth - 23))}\u2510
|
|
4069
4287
|
`));
|
|
@@ -4072,7 +4290,7 @@ var ApprovalGate = class {
|
|
|
4072
4290
|
process.stdout.write(chalk5.yellow(` \u2514${topBar}\u2518
|
|
4073
4291
|
`));
|
|
4074
4292
|
process.stdout.write(
|
|
4075
|
-
` ${
|
|
4293
|
+
` ${allowLabel} allow ${chalk5.cyan("[a]")} always ${chalk5.red("[n]")} deny ${chalk5.yellow("\u203A")} `
|
|
4076
4294
|
);
|
|
4077
4295
|
const answer = await ask();
|
|
4078
4296
|
if (answer === null) {
|
|
@@ -4085,7 +4303,7 @@ var ApprovalGate = class {
|
|
|
4085
4303
|
process.stdout.write(chalk5.green(" \u2713 Always allowed.\n\n"));
|
|
4086
4304
|
return true;
|
|
4087
4305
|
}
|
|
4088
|
-
if (trimmed === "y" || trimmed === "yes") {
|
|
4306
|
+
if (trimmed === "y" || trimmed === "yes" || trimmed === "" && defaultAllow) {
|
|
4089
4307
|
process.stdout.write(chalk5.green(" \u2713 Allowed.\n\n"));
|
|
4090
4308
|
return true;
|
|
4091
4309
|
}
|
|
@@ -4127,6 +4345,9 @@ function formatSummary(toolName, input) {
|
|
|
4127
4345
|
case "edit":
|
|
4128
4346
|
raw = `edit ${input.file_path}`;
|
|
4129
4347
|
break;
|
|
4348
|
+
case "web_search":
|
|
4349
|
+
raw = `Copair web search "${input.query}"`;
|
|
4350
|
+
break;
|
|
4130
4351
|
default:
|
|
4131
4352
|
raw = `${toolName} ${JSON.stringify(input)}`;
|
|
4132
4353
|
break;
|
|
@@ -4656,7 +4877,7 @@ function ApprovalHandler({ bridge }) {
|
|
|
4656
4877
|
}
|
|
4657
4878
|
|
|
4658
4879
|
// src/cli/ui/app.tsx
|
|
4659
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
4880
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
4660
4881
|
var DEFAULT_UI_CONFIG = {
|
|
4661
4882
|
bordered_input: true,
|
|
4662
4883
|
status_bar: true,
|
|
@@ -4689,6 +4910,139 @@ function useSpinner(active) {
|
|
|
4689
4910
|
const elapsedStr = secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
4690
4911
|
return { frame: SPINNER_FRAMES[frameIdx], elapsed: elapsedStr };
|
|
4691
4912
|
}
|
|
4913
|
+
function renderInline(text) {
|
|
4914
|
+
const parts = [];
|
|
4915
|
+
let remaining = text;
|
|
4916
|
+
let key = 0;
|
|
4917
|
+
while (remaining.length > 0) {
|
|
4918
|
+
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
|
4919
|
+
if (boldMatch) {
|
|
4920
|
+
parts.push(/* @__PURE__ */ jsx6(Text6, { bold: true, children: boldMatch[1] }, key++));
|
|
4921
|
+
remaining = remaining.slice(boldMatch[0].length);
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
const italicMatch = remaining.match(/^\*(.+?)\*/);
|
|
4925
|
+
if (italicMatch) {
|
|
4926
|
+
parts.push(/* @__PURE__ */ jsx6(Text6, { italic: true, children: italicMatch[1] }, key++));
|
|
4927
|
+
remaining = remaining.slice(italicMatch[0].length);
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4930
|
+
const codeMatch = remaining.match(/^`([^`]+)`/);
|
|
4931
|
+
if (codeMatch) {
|
|
4932
|
+
parts.push(/* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: codeMatch[1] }, key++));
|
|
4933
|
+
remaining = remaining.slice(codeMatch[0].length);
|
|
4934
|
+
continue;
|
|
4935
|
+
}
|
|
4936
|
+
const nextSpecial = remaining.search(/[*`]/);
|
|
4937
|
+
if (nextSpecial === -1) {
|
|
4938
|
+
parts.push(remaining);
|
|
4939
|
+
break;
|
|
4940
|
+
}
|
|
4941
|
+
if (nextSpecial === 0) {
|
|
4942
|
+
parts.push(remaining[0]);
|
|
4943
|
+
remaining = remaining.slice(1);
|
|
4944
|
+
} else {
|
|
4945
|
+
parts.push(remaining.slice(0, nextSpecial));
|
|
4946
|
+
remaining = remaining.slice(nextSpecial);
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
return parts.length === 1 ? parts[0] : /* @__PURE__ */ jsx6(Fragment2, { children: parts });
|
|
4950
|
+
}
|
|
4951
|
+
function renderMarkdownBlocks(text) {
|
|
4952
|
+
const lines = text.split("\n");
|
|
4953
|
+
const elements = [];
|
|
4954
|
+
let key = 0;
|
|
4955
|
+
let i = 0;
|
|
4956
|
+
while (i < lines.length) {
|
|
4957
|
+
const line = lines[i];
|
|
4958
|
+
const trimmed = line.trim();
|
|
4959
|
+
if (trimmed.startsWith("```")) {
|
|
4960
|
+
const lang = trimmed.slice(3).trim();
|
|
4961
|
+
const codeLines = [];
|
|
4962
|
+
i++;
|
|
4963
|
+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
|
4964
|
+
codeLines.push(lines[i]);
|
|
4965
|
+
i++;
|
|
4966
|
+
}
|
|
4967
|
+
if (i < lines.length) i++;
|
|
4968
|
+
elements.push(
|
|
4969
|
+
/* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginY: 1, children: [
|
|
4970
|
+
lang && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: lang }),
|
|
4971
|
+
/* @__PURE__ */ jsx6(Box6, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: codeLines.map((cl, ci) => /* @__PURE__ */ jsx6(Text6, { color: "white", children: cl }, ci)) })
|
|
4972
|
+
] }, key++)
|
|
4973
|
+
);
|
|
4974
|
+
continue;
|
|
4975
|
+
}
|
|
4976
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)/);
|
|
4977
|
+
if (headerMatch) {
|
|
4978
|
+
const level = headerMatch[1].length;
|
|
4979
|
+
const content = headerMatch[2];
|
|
4980
|
+
elements.push(
|
|
4981
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: level <= 2 ? "white" : void 0, children: [
|
|
4982
|
+
level <= 2 ? "\n" : "",
|
|
4983
|
+
content
|
|
4984
|
+
] }, key++)
|
|
4985
|
+
);
|
|
4986
|
+
i++;
|
|
4987
|
+
continue;
|
|
4988
|
+
}
|
|
4989
|
+
if (/^[-*_]{3,}$/.test(trimmed)) {
|
|
4990
|
+
elements.push(
|
|
4991
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(40) }, key++)
|
|
4992
|
+
);
|
|
4993
|
+
i++;
|
|
4994
|
+
continue;
|
|
4995
|
+
}
|
|
4996
|
+
const ulMatch = trimmed.match(/^[-*+]\s+(.*)/);
|
|
4997
|
+
if (ulMatch) {
|
|
4998
|
+
elements.push(
|
|
4999
|
+
/* @__PURE__ */ jsxs6(Text6, { wrap: "wrap", children: [
|
|
5000
|
+
" ",
|
|
5001
|
+
"\u2022",
|
|
5002
|
+
" ",
|
|
5003
|
+
renderInline(ulMatch[1])
|
|
5004
|
+
] }, key++)
|
|
5005
|
+
);
|
|
5006
|
+
i++;
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
const olMatch = trimmed.match(/^(\d+)[.)]\s+(.*)/);
|
|
5010
|
+
if (olMatch) {
|
|
5011
|
+
elements.push(
|
|
5012
|
+
/* @__PURE__ */ jsxs6(Text6, { wrap: "wrap", children: [
|
|
5013
|
+
" ",
|
|
5014
|
+
olMatch[1],
|
|
5015
|
+
". ",
|
|
5016
|
+
renderInline(olMatch[2])
|
|
5017
|
+
] }, key++)
|
|
5018
|
+
);
|
|
5019
|
+
i++;
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
if (trimmed.startsWith(">")) {
|
|
5023
|
+
const content = trimmed.replace(/^>\s?/, "");
|
|
5024
|
+
elements.push(
|
|
5025
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "wrap", children: [
|
|
5026
|
+
" ",
|
|
5027
|
+
"\u2502",
|
|
5028
|
+
" ",
|
|
5029
|
+
renderInline(content)
|
|
5030
|
+
] }, key++)
|
|
5031
|
+
);
|
|
5032
|
+
i++;
|
|
5033
|
+
continue;
|
|
5034
|
+
}
|
|
5035
|
+
if (trimmed === "") {
|
|
5036
|
+
i++;
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
elements.push(
|
|
5040
|
+
/* @__PURE__ */ jsx6(Text6, { wrap: "wrap", children: renderInline(line) }, key++)
|
|
5041
|
+
);
|
|
5042
|
+
i++;
|
|
5043
|
+
}
|
|
5044
|
+
return elements;
|
|
5045
|
+
}
|
|
4692
5046
|
var CopairApp = forwardRef(function CopairApp2({
|
|
4693
5047
|
bridge,
|
|
4694
5048
|
model,
|
|
@@ -4786,6 +5140,12 @@ var CopairApp = forwardRef(function CopairApp2({
|
|
|
4786
5140
|
{ id: nextId.current++, type: "error", content: `\u2717 ${tool.label} denied` }
|
|
4787
5141
|
]);
|
|
4788
5142
|
};
|
|
5143
|
+
const onDiff = (diff) => {
|
|
5144
|
+
setStaticItems((items) => [
|
|
5145
|
+
...items,
|
|
5146
|
+
{ id: nextId.current++, type: "diff", content: "", diff }
|
|
5147
|
+
]);
|
|
5148
|
+
};
|
|
4789
5149
|
const onError = (message) => {
|
|
4790
5150
|
setStaticItems((items) => [
|
|
4791
5151
|
...items,
|
|
@@ -4816,6 +5176,7 @@ var CopairApp = forwardRef(function CopairApp2({
|
|
|
4816
5176
|
bridge.on("tool-start", onToolStart);
|
|
4817
5177
|
bridge.on("tool-complete", onToolComplete);
|
|
4818
5178
|
bridge.on("tool-denied", onToolDenied);
|
|
5179
|
+
bridge.on("diff", onDiff);
|
|
4819
5180
|
bridge.on("error", onError);
|
|
4820
5181
|
bridge.on("usage", onUsage);
|
|
4821
5182
|
bridge.on("turn-complete", onTurnComplete);
|
|
@@ -4825,6 +5186,7 @@ var CopairApp = forwardRef(function CopairApp2({
|
|
|
4825
5186
|
bridge.off("tool-start", onToolStart);
|
|
4826
5187
|
bridge.off("tool-complete", onToolComplete);
|
|
4827
5188
|
bridge.off("tool-denied", onToolDenied);
|
|
5189
|
+
bridge.off("diff", onDiff);
|
|
4828
5190
|
bridge.off("error", onError);
|
|
4829
5191
|
bridge.off("usage", onUsage);
|
|
4830
5192
|
bridge.off("turn-complete", onTurnComplete);
|
|
@@ -4860,9 +5222,11 @@ var CopairApp = forwardRef(function CopairApp2({
|
|
|
4860
5222
|
" ",
|
|
4861
5223
|
item.content
|
|
4862
5224
|
] }, item.id);
|
|
5225
|
+
case "diff":
|
|
5226
|
+
return item.diff ? /* @__PURE__ */ jsx6(DiffView, { diff: item.diff }, item.id) : null;
|
|
4863
5227
|
case "text":
|
|
4864
5228
|
default:
|
|
4865
|
-
return /* @__PURE__ */ jsx6(
|
|
5229
|
+
return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: renderMarkdownBlocks(item.content) }, item.id);
|
|
4866
5230
|
}
|
|
4867
5231
|
} }),
|
|
4868
5232
|
state.phase === "thinking" && /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
@@ -4874,7 +5238,7 @@ var CopairApp = forwardRef(function CopairApp2({
|
|
|
4874
5238
|
/* @__PURE__ */ jsx6(Text6, { color: "gray", children: spinner.elapsed })
|
|
4875
5239
|
] })
|
|
4876
5240
|
] }),
|
|
4877
|
-
liveText && /* @__PURE__ */ jsx6(
|
|
5241
|
+
liveText && /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: renderMarkdownBlocks(liveText) }),
|
|
4878
5242
|
liveTool && /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
4879
5243
|
" ",
|
|
4880
5244
|
"\u25CF",
|
|
@@ -4967,7 +5331,7 @@ var ToolExecutor = class {
|
|
|
4967
5331
|
};
|
|
4968
5332
|
|
|
4969
5333
|
// src/core/allow-list.ts
|
|
4970
|
-
import { readFileSync as
|
|
5334
|
+
import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
|
|
4971
5335
|
import { resolve as resolve8 } from "path";
|
|
4972
5336
|
import { homedir as homedir2 } from "os";
|
|
4973
5337
|
import { parse as parseYaml3 } from "yaml";
|
|
@@ -5036,9 +5400,9 @@ function loadAllowList(projectDir) {
|
|
|
5036
5400
|
});
|
|
5037
5401
|
}
|
|
5038
5402
|
function readAllowFile(filePath) {
|
|
5039
|
-
if (!
|
|
5403
|
+
if (!existsSync9(filePath)) return {};
|
|
5040
5404
|
try {
|
|
5041
|
-
const raw = parseYaml3(
|
|
5405
|
+
const raw = parseYaml3(readFileSync5(filePath, "utf-8"));
|
|
5042
5406
|
return {
|
|
5043
5407
|
bash: toStringArray(raw.bash),
|
|
5044
5408
|
git: toStringArray(raw.git),
|
|
@@ -5084,7 +5448,7 @@ import chalk6 from "chalk";
|
|
|
5084
5448
|
// package.json
|
|
5085
5449
|
var package_default = {
|
|
5086
5450
|
name: "@dugleelabs/copair",
|
|
5087
|
-
version: "1.0
|
|
5451
|
+
version: "1.1.0",
|
|
5088
5452
|
description: "Model-agnostic AI coding agent for the terminal",
|
|
5089
5453
|
type: "module",
|
|
5090
5454
|
main: "dist/index.js",
|
|
@@ -5241,20 +5605,20 @@ var DEFAULT_PRICING = /* @__PURE__ */ new Map([
|
|
|
5241
5605
|
]);
|
|
5242
5606
|
|
|
5243
5607
|
// src/cli/ui/input-history.ts
|
|
5244
|
-
import { readFileSync as
|
|
5245
|
-
import { join as
|
|
5608
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
|
|
5609
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
5246
5610
|
import { homedir as homedir3 } from "os";
|
|
5247
5611
|
var MAX_HISTORY = 500;
|
|
5248
5612
|
function resolveHistoryPath(cwd) {
|
|
5249
|
-
const projectPath =
|
|
5250
|
-
if (
|
|
5613
|
+
const projectPath = join6(cwd, ".copair", "history");
|
|
5614
|
+
if (existsSync10(join6(cwd, ".copair"))) {
|
|
5251
5615
|
return projectPath;
|
|
5252
5616
|
}
|
|
5253
|
-
return
|
|
5617
|
+
return join6(homedir3(), ".copair", "history");
|
|
5254
5618
|
}
|
|
5255
5619
|
function loadHistory(historyPath) {
|
|
5256
5620
|
try {
|
|
5257
|
-
const content =
|
|
5621
|
+
const content = readFileSync6(historyPath, "utf-8");
|
|
5258
5622
|
return content.split("\n").filter(Boolean);
|
|
5259
5623
|
} catch {
|
|
5260
5624
|
return [];
|
|
@@ -5263,10 +5627,10 @@ function loadHistory(historyPath) {
|
|
|
5263
5627
|
function saveHistory(historyPath, entries) {
|
|
5264
5628
|
const trimmed = entries.slice(-MAX_HISTORY);
|
|
5265
5629
|
const dir = dirname4(historyPath);
|
|
5266
|
-
if (!
|
|
5267
|
-
|
|
5630
|
+
if (!existsSync10(dir)) {
|
|
5631
|
+
mkdirSync3(dir, { recursive: true });
|
|
5268
5632
|
}
|
|
5269
|
-
|
|
5633
|
+
writeFileSync3(historyPath, trimmed.join("\n") + "\n", "utf-8");
|
|
5270
5634
|
}
|
|
5271
5635
|
function appendHistory(historyPath, entry) {
|
|
5272
5636
|
const entries = loadHistory(historyPath);
|
|
@@ -5278,7 +5642,7 @@ function appendHistory(historyPath, entry) {
|
|
|
5278
5642
|
|
|
5279
5643
|
// src/cli/ui/completion-providers.ts
|
|
5280
5644
|
import { readdirSync } from "fs";
|
|
5281
|
-
import { join as
|
|
5645
|
+
import { join as join7, dirname as dirname5, basename } from "path";
|
|
5282
5646
|
var SlashCommandProvider = class {
|
|
5283
5647
|
id = "slash-commands";
|
|
5284
5648
|
commands;
|
|
@@ -5316,7 +5680,7 @@ var FilePathProvider = class {
|
|
|
5316
5680
|
complete(input) {
|
|
5317
5681
|
const lastToken = input.split(/\s+/).pop() ?? "";
|
|
5318
5682
|
try {
|
|
5319
|
-
const dir = lastToken.endsWith("/") ?
|
|
5683
|
+
const dir = lastToken.endsWith("/") ? join7(this.cwd, lastToken) : join7(this.cwd, dirname5(lastToken));
|
|
5320
5684
|
const prefix = lastToken.endsWith("/") ? "" : basename(lastToken);
|
|
5321
5685
|
const beforeToken = input.slice(0, input.length - lastToken.length);
|
|
5322
5686
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -5368,6 +5732,447 @@ var CompletionEngine = class {
|
|
|
5368
5732
|
}
|
|
5369
5733
|
};
|
|
5370
5734
|
|
|
5735
|
+
// src/init/GlobalInitManager.ts
|
|
5736
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5737
|
+
import { join as join8 } from "path";
|
|
5738
|
+
import { homedir as homedir4 } from "os";
|
|
5739
|
+
import * as readline from "readline";
|
|
5740
|
+
var GLOBAL_CONFIG_TEMPLATE = `# Copair global configuration
|
|
5741
|
+
# Generated by Copair on first run \u2014 edit as needed
|
|
5742
|
+
|
|
5743
|
+
# provider:
|
|
5744
|
+
# name: anthropic # anthropic | openai | google | ollama
|
|
5745
|
+
# model: claude-sonnet-4-6
|
|
5746
|
+
# api_key: your-api-key-here
|
|
5747
|
+
# endpoint: ~ # optional override (e.g. for local Ollama)
|
|
5748
|
+
|
|
5749
|
+
# identity:
|
|
5750
|
+
# name: ~ # used in git co-author trailers
|
|
5751
|
+
# email: ~
|
|
5752
|
+
|
|
5753
|
+
# ui:
|
|
5754
|
+
# status_bar: true
|
|
5755
|
+
# syntax_highlight: true
|
|
5756
|
+
# vi_mode: false
|
|
5757
|
+
# bordered_input: true
|
|
5758
|
+
|
|
5759
|
+
# permissions:
|
|
5760
|
+
# mode: ask # ask | auto | deny
|
|
5761
|
+
|
|
5762
|
+
# context:
|
|
5763
|
+
# summarization_model: ~ # model used for session summarisation
|
|
5764
|
+
# max_sessions: 50
|
|
5765
|
+
`;
|
|
5766
|
+
function prompt(question) {
|
|
5767
|
+
const rl = readline.createInterface({
|
|
5768
|
+
input: process.stdin,
|
|
5769
|
+
output: process.stdout
|
|
5770
|
+
});
|
|
5771
|
+
return new Promise((resolve9) => {
|
|
5772
|
+
rl.question(question, (answer) => {
|
|
5773
|
+
rl.close();
|
|
5774
|
+
resolve9(answer.trim().toLowerCase());
|
|
5775
|
+
});
|
|
5776
|
+
});
|
|
5777
|
+
}
|
|
5778
|
+
var GlobalInitManager = class {
|
|
5779
|
+
globalDir;
|
|
5780
|
+
constructor(homeDir) {
|
|
5781
|
+
this.globalDir = join8(homeDir ?? homedir4(), ".copair");
|
|
5782
|
+
}
|
|
5783
|
+
async check(options = { ci: false }) {
|
|
5784
|
+
if (existsSync11(this.globalDir)) {
|
|
5785
|
+
return { skipped: true, declined: false, created: false };
|
|
5786
|
+
}
|
|
5787
|
+
if (options.ci) {
|
|
5788
|
+
return { skipped: false, declined: true, created: false };
|
|
5789
|
+
}
|
|
5790
|
+
const answer = await prompt("Set up global Copair config at ~/.copair/? (Y/n) ");
|
|
5791
|
+
const declined = answer === "n" || answer === "no";
|
|
5792
|
+
if (declined) {
|
|
5793
|
+
return { skipped: false, declined: true, created: false };
|
|
5794
|
+
}
|
|
5795
|
+
await this.scaffold();
|
|
5796
|
+
return { skipped: false, declined: false, created: true };
|
|
5797
|
+
}
|
|
5798
|
+
async scaffold() {
|
|
5799
|
+
mkdirSync4(this.globalDir, { recursive: true });
|
|
5800
|
+
const configPath = join8(this.globalDir, "config.yaml");
|
|
5801
|
+
if (!existsSync11(configPath)) {
|
|
5802
|
+
writeFileSync4(configPath, GLOBAL_CONFIG_TEMPLATE, { mode: 384 });
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
};
|
|
5806
|
+
|
|
5807
|
+
// src/init/ProjectInitManager.ts
|
|
5808
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
5809
|
+
import { join as join9 } from "path";
|
|
5810
|
+
import * as readline2 from "readline";
|
|
5811
|
+
var PROJECT_CONFIG_TEMPLATE = `# Copair project configuration
|
|
5812
|
+
# Overrides ~/.copair/config.yaml for this project
|
|
5813
|
+
# This file is gitignored \u2014 do not commit
|
|
5814
|
+
|
|
5815
|
+
# provider:
|
|
5816
|
+
# model: ~ # override model for this project
|
|
5817
|
+
|
|
5818
|
+
# permissions:
|
|
5819
|
+
# mode: ask
|
|
5820
|
+
`;
|
|
5821
|
+
function prompt2(question) {
|
|
5822
|
+
const rl = readline2.createInterface({
|
|
5823
|
+
input: process.stdin,
|
|
5824
|
+
output: process.stdout
|
|
5825
|
+
});
|
|
5826
|
+
return new Promise((resolve9) => {
|
|
5827
|
+
rl.question(question, (answer) => {
|
|
5828
|
+
rl.close();
|
|
5829
|
+
resolve9(answer.trim().toLowerCase());
|
|
5830
|
+
});
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
var ProjectInitManager = class {
|
|
5834
|
+
async check(cwd, options) {
|
|
5835
|
+
const copairDir = join9(cwd, ".copair");
|
|
5836
|
+
if (existsSync12(copairDir)) {
|
|
5837
|
+
return { alreadyInitialised: true, declined: false, created: false };
|
|
5838
|
+
}
|
|
5839
|
+
if (options.ci) {
|
|
5840
|
+
process.stderr.write(
|
|
5841
|
+
"Copair: .copair/ not found. In CI mode, automatic init is skipped.\nRun copair interactively once to initialise this project.\n"
|
|
5842
|
+
);
|
|
5843
|
+
return { alreadyInitialised: false, declined: true, created: false };
|
|
5844
|
+
}
|
|
5845
|
+
const answer = await prompt2("Trust this folder and allow Copair to run here? (y/N) ");
|
|
5846
|
+
const accepted = answer === "y" || answer === "yes";
|
|
5847
|
+
if (!accepted) {
|
|
5848
|
+
return { alreadyInitialised: false, declined: true, created: false };
|
|
5849
|
+
}
|
|
5850
|
+
await this.scaffold(cwd);
|
|
5851
|
+
return { alreadyInitialised: false, declined: false, created: true };
|
|
5852
|
+
}
|
|
5853
|
+
async scaffold(cwd) {
|
|
5854
|
+
const copairDir = join9(cwd, ".copair");
|
|
5855
|
+
mkdirSync5(join9(copairDir, "commands"), { recursive: true });
|
|
5856
|
+
const configPath = join9(copairDir, "config.yaml");
|
|
5857
|
+
if (!existsSync12(configPath)) {
|
|
5858
|
+
writeFileSync5(configPath, PROJECT_CONFIG_TEMPLATE, { mode: 420 });
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
var DECLINED_MESSAGE = "Copair not initialised. Run copair again in a trusted folder.";
|
|
5863
|
+
|
|
5864
|
+
// src/init/GitignoreManager.ts
|
|
5865
|
+
import { existsSync as existsSync13, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
5866
|
+
import { join as join10 } from "path";
|
|
5867
|
+
import * as readline3 from "readline";
|
|
5868
|
+
var FULL_PATTERNS = [".copair/", ".copair"];
|
|
5869
|
+
function prompt3(question) {
|
|
5870
|
+
const rl = readline3.createInterface({
|
|
5871
|
+
input: process.stdin,
|
|
5872
|
+
output: process.stdout
|
|
5873
|
+
});
|
|
5874
|
+
return new Promise((resolve9) => {
|
|
5875
|
+
rl.question(question, (answer) => {
|
|
5876
|
+
rl.close();
|
|
5877
|
+
resolve9(answer.trim().toLowerCase());
|
|
5878
|
+
});
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
var GitignoreManager = class {
|
|
5882
|
+
/**
|
|
5883
|
+
* Owns the full classify → prompt → consolidate flow.
|
|
5884
|
+
* Runs on every startup. Skips silently if already fully covered.
|
|
5885
|
+
* In CI mode applies consolidation silently without prompting.
|
|
5886
|
+
*/
|
|
5887
|
+
async ensureCovered(cwd, options) {
|
|
5888
|
+
const coverage = await this.classify(cwd);
|
|
5889
|
+
if (coverage === "full") return;
|
|
5890
|
+
if (options.ci) {
|
|
5891
|
+
await this.consolidate(cwd);
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
const answer = await prompt3("Add .copair/ to .gitignore? (Y/n) ");
|
|
5895
|
+
const declined = answer === "n" || answer === "no";
|
|
5896
|
+
if (!declined) {
|
|
5897
|
+
await this.consolidate(cwd);
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
async classify(cwd) {
|
|
5901
|
+
const gitignorePath = join10(cwd, ".gitignore");
|
|
5902
|
+
if (!existsSync13(gitignorePath)) return "none";
|
|
5903
|
+
const lines = readFileSync7(gitignorePath, "utf8").split(/\r?\n/).map((l) => l.trim());
|
|
5904
|
+
for (const line of lines) {
|
|
5905
|
+
if (FULL_PATTERNS.includes(line)) return "full";
|
|
5906
|
+
}
|
|
5907
|
+
const hasPartial = lines.some(
|
|
5908
|
+
(l) => l.startsWith(".copair/") && !FULL_PATTERNS.includes(l)
|
|
5909
|
+
);
|
|
5910
|
+
return hasPartial ? "partial" : "none";
|
|
5911
|
+
}
|
|
5912
|
+
async consolidate(cwd) {
|
|
5913
|
+
const gitignorePath = join10(cwd, ".gitignore");
|
|
5914
|
+
let lines = [];
|
|
5915
|
+
if (existsSync13(gitignorePath)) {
|
|
5916
|
+
lines = readFileSync7(gitignorePath, "utf8").split(/\r?\n/);
|
|
5917
|
+
}
|
|
5918
|
+
const filtered = lines.filter((l) => {
|
|
5919
|
+
const trimmed = l.trim();
|
|
5920
|
+
return !trimmed.startsWith(".copair/") || FULL_PATTERNS.includes(trimmed);
|
|
5921
|
+
});
|
|
5922
|
+
while (filtered.length > 0 && filtered[filtered.length - 1].trim() === "") {
|
|
5923
|
+
filtered.pop();
|
|
5924
|
+
}
|
|
5925
|
+
filtered.push("", "# Copair runtime state", ".copair/", "");
|
|
5926
|
+
writeFileSync6(gitignorePath, filtered.join("\n"), { encoding: "utf8" });
|
|
5927
|
+
}
|
|
5928
|
+
};
|
|
5929
|
+
|
|
5930
|
+
// src/knowledge/KnowledgeManager.ts
|
|
5931
|
+
import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5932
|
+
import { join as join11 } from "path";
|
|
5933
|
+
import * as readline4 from "readline";
|
|
5934
|
+
var KB_FILENAME2 = "COPAIR_KNOWLEDGE.md";
|
|
5935
|
+
var DEFAULT_CONFIG = {
|
|
5936
|
+
warn_size_kb: 8,
|
|
5937
|
+
max_size_kb: 16
|
|
5938
|
+
};
|
|
5939
|
+
var TRIGGER_PATTERNS = [
|
|
5940
|
+
/^[^/]+\/$/,
|
|
5941
|
+
// new top-level directory
|
|
5942
|
+
/(?:^|\/)(?:index|main|app|server|bin\/)\.[jt]sx?$/,
|
|
5943
|
+
// entry points
|
|
5944
|
+
/(?:^|\/)(?:package\.json|tsconfig.*\.json|\.env\.example|Dockerfile|docker-compose\.ya?ml)$/
|
|
5945
|
+
// config files
|
|
5946
|
+
];
|
|
5947
|
+
var SKIP_PATTERNS = [
|
|
5948
|
+
/(?:^|\/)tests?\//,
|
|
5949
|
+
// test files
|
|
5950
|
+
/\.test\.[jt]sx?$/,
|
|
5951
|
+
/\.spec\.[jt]sx?$/
|
|
5952
|
+
];
|
|
5953
|
+
function promptUser(question) {
|
|
5954
|
+
const rl = readline4.createInterface({
|
|
5955
|
+
input: process.stdin,
|
|
5956
|
+
output: process.stdout
|
|
5957
|
+
});
|
|
5958
|
+
return new Promise((resolve9) => {
|
|
5959
|
+
rl.question(question, (answer) => {
|
|
5960
|
+
rl.close();
|
|
5961
|
+
resolve9(answer.trim().toLowerCase());
|
|
5962
|
+
});
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
var KnowledgeManager = class {
|
|
5966
|
+
config;
|
|
5967
|
+
constructor(config = {}) {
|
|
5968
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
5969
|
+
}
|
|
5970
|
+
load(cwd) {
|
|
5971
|
+
const filePath = join11(cwd, KB_FILENAME2);
|
|
5972
|
+
if (!existsSync14(filePath)) {
|
|
5973
|
+
return { found: false, content: null, sizeBytes: 0 };
|
|
5974
|
+
}
|
|
5975
|
+
try {
|
|
5976
|
+
const content = readFileSync8(filePath, "utf8");
|
|
5977
|
+
const sizeBytes = Buffer.byteLength(content, "utf8");
|
|
5978
|
+
return { found: true, content, sizeBytes };
|
|
5979
|
+
} catch {
|
|
5980
|
+
return { found: false, content: null, sizeBytes: 0 };
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
injectIntoSystemPrompt(content) {
|
|
5984
|
+
return `<knowledge>
|
|
5985
|
+
${content.trim()}
|
|
5986
|
+
</knowledge>
|
|
5987
|
+
|
|
5988
|
+
`;
|
|
5989
|
+
}
|
|
5990
|
+
checkSizeBudget(sizeBytes) {
|
|
5991
|
+
const warnBytes = this.config.warn_size_kb * 1024;
|
|
5992
|
+
const maxBytes = this.config.max_size_kb * 1024;
|
|
5993
|
+
if (sizeBytes > maxBytes) {
|
|
5994
|
+
throw new Error(
|
|
5995
|
+
`COPAIR_KNOWLEDGE.md exceeds the ${this.config.max_size_kb} KB hard cap (${Math.round(sizeBytes / 1024)} KB). Reduce the file size before starting a session.`
|
|
5996
|
+
);
|
|
5997
|
+
}
|
|
5998
|
+
if (sizeBytes > warnBytes) {
|
|
5999
|
+
process.stderr.write(
|
|
6000
|
+
`[knowledge] Warning: COPAIR_KNOWLEDGE.md is ${Math.round(sizeBytes / 1024)} KB (recommended max: ${this.config.warn_size_kb} KB). Consider trimming it to keep prompts efficient.
|
|
6001
|
+
`
|
|
6002
|
+
);
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
/**
|
|
6006
|
+
* Evaluate whether the knowledge file needs updating after a task.
|
|
6007
|
+
* Returns a proposed update description if an update is warranted, null otherwise.
|
|
6008
|
+
*/
|
|
6009
|
+
evaluateForUpdate(filesChanged, _diff) {
|
|
6010
|
+
if (filesChanged.length === 0) return null;
|
|
6011
|
+
const nonTestFiles = filesChanged.filter(
|
|
6012
|
+
(f) => !SKIP_PATTERNS.some((p) => p.test(f))
|
|
6013
|
+
);
|
|
6014
|
+
if (nonTestFiles.length === 0) return null;
|
|
6015
|
+
const triggers = nonTestFiles.filter(
|
|
6016
|
+
(f) => TRIGGER_PATTERNS.some((p) => p.test(f))
|
|
6017
|
+
);
|
|
6018
|
+
if (triggers.length === 0) return null;
|
|
6019
|
+
return `The following changes may affect the knowledge file:
|
|
6020
|
+
` + triggers.map((f) => ` - ${f}`).join("\n") + "\nConsider updating COPAIR_KNOWLEDGE.md to reflect these changes.";
|
|
6021
|
+
}
|
|
6022
|
+
async proposeUpdate(cwd, proposedDiff) {
|
|
6023
|
+
process.stdout.write(
|
|
6024
|
+
"\n[knowledge] Proposed update to COPAIR_KNOWLEDGE.md:\n\n" + proposedDiff + "\n"
|
|
6025
|
+
);
|
|
6026
|
+
const answer = await promptUser("Apply this update to COPAIR_KNOWLEDGE.md? (Y/n) ");
|
|
6027
|
+
const declined = answer === "n" || answer === "no";
|
|
6028
|
+
if (declined) return false;
|
|
6029
|
+
await this.applyUpdate(cwd, proposedDiff);
|
|
6030
|
+
return true;
|
|
6031
|
+
}
|
|
6032
|
+
applyUpdate(cwd, content) {
|
|
6033
|
+
const filePath = join11(cwd, KB_FILENAME2);
|
|
6034
|
+
const sizeBytes = Buffer.byteLength(content, "utf8");
|
|
6035
|
+
const maxBytes = this.config.max_size_kb * 1024;
|
|
6036
|
+
if (sizeBytes > maxBytes) {
|
|
6037
|
+
throw new Error(
|
|
6038
|
+
`Cannot apply update: result would be ${Math.round(sizeBytes / 1024)} KB, exceeding the ${this.config.max_size_kb} KB cap.`
|
|
6039
|
+
);
|
|
6040
|
+
}
|
|
6041
|
+
writeFileSync7(filePath, content, { encoding: "utf8", mode: 420 });
|
|
6042
|
+
}
|
|
6043
|
+
};
|
|
6044
|
+
|
|
6045
|
+
// src/knowledge/KnowledgeSetupFlow.ts
|
|
6046
|
+
import { writeFileSync as writeFileSync8 } from "fs";
|
|
6047
|
+
import { join as join12 } from "path";
|
|
6048
|
+
import * as readline5 from "readline";
|
|
6049
|
+
var SECTIONS = [
|
|
6050
|
+
{
|
|
6051
|
+
key: "directory-map",
|
|
6052
|
+
heading: "## Directory Map",
|
|
6053
|
+
question: 'What are the key directories in this project and what does each own?\n(e.g. "src/ \u2014 all TypeScript source", "bin/ \u2014 CLI entry point")',
|
|
6054
|
+
skippable: false
|
|
6055
|
+
},
|
|
6056
|
+
{
|
|
6057
|
+
key: "tech-stack",
|
|
6058
|
+
heading: "## Tech Stack",
|
|
6059
|
+
question: 'What language, runtime, and key frameworks are in use?\n(e.g. "TypeScript / Node.js 20+, pnpm, vitest")',
|
|
6060
|
+
skippable: false
|
|
6061
|
+
},
|
|
6062
|
+
{
|
|
6063
|
+
key: "naming-conventions",
|
|
6064
|
+
heading: "## Naming Conventions",
|
|
6065
|
+
question: 'Any naming conventions for files, components, variables, or API routes?\n(Type "skip" to omit this section)',
|
|
6066
|
+
skippable: true
|
|
6067
|
+
},
|
|
6068
|
+
{
|
|
6069
|
+
key: "entry-points",
|
|
6070
|
+
heading: "## Entry Points",
|
|
6071
|
+
question: 'What are the key entry points \u2014 main file, config files, bootstrap?\n(e.g. "bin/copair.ts \u2014 CLI entry", "src/session/SessionBootstrap.ts \u2014 startup")',
|
|
6072
|
+
skippable: false
|
|
6073
|
+
},
|
|
6074
|
+
{
|
|
6075
|
+
key: "off-limits",
|
|
6076
|
+
heading: "## Off-Limits",
|
|
6077
|
+
question: 'Any files or directories Copair must not touch without explicit instruction?\n(Type "skip" to omit this section)',
|
|
6078
|
+
skippable: true
|
|
6079
|
+
}
|
|
6080
|
+
];
|
|
6081
|
+
function createRl() {
|
|
6082
|
+
return readline5.createInterface({
|
|
6083
|
+
input: process.stdin,
|
|
6084
|
+
output: process.stdout
|
|
6085
|
+
});
|
|
6086
|
+
}
|
|
6087
|
+
async function ask2(question) {
|
|
6088
|
+
const rl = createRl();
|
|
6089
|
+
return new Promise((resolve9) => {
|
|
6090
|
+
rl.question(question + "\n> ", (answer) => {
|
|
6091
|
+
rl.close();
|
|
6092
|
+
resolve9(answer.trim());
|
|
6093
|
+
});
|
|
6094
|
+
});
|
|
6095
|
+
}
|
|
6096
|
+
async function confirm(question) {
|
|
6097
|
+
const rl = createRl();
|
|
6098
|
+
return new Promise((resolve9) => {
|
|
6099
|
+
rl.question(question, (answer) => {
|
|
6100
|
+
rl.close();
|
|
6101
|
+
const lower = answer.trim().toLowerCase();
|
|
6102
|
+
resolve9(lower !== "n" && lower !== "no");
|
|
6103
|
+
});
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
var KnowledgeSetupFlow = class {
|
|
6107
|
+
/**
|
|
6108
|
+
* Prompts the user to set up a COPAIR_KNOWLEDGE.md.
|
|
6109
|
+
* Returns true if a file was written, false if the user declined.
|
|
6110
|
+
*/
|
|
6111
|
+
async run(cwd) {
|
|
6112
|
+
const shouldSetup = await confirm(
|
|
6113
|
+
"No knowledge file found. Set one up now? (Y/n) "
|
|
6114
|
+
);
|
|
6115
|
+
if (!shouldSetup) return false;
|
|
6116
|
+
process.stdout.write(
|
|
6117
|
+
"\nLet's build your COPAIR_KNOWLEDGE.md \u2014 a navigation map for Copair.\nAnswer each section (press Enter to confirm).\n\n"
|
|
6118
|
+
);
|
|
6119
|
+
const sections = [];
|
|
6120
|
+
for (const section of SECTIONS) {
|
|
6121
|
+
process.stdout.write(`--- ${section.heading.replace("## ", "")} ---
|
|
6122
|
+
`);
|
|
6123
|
+
const answer = await ask2(section.question);
|
|
6124
|
+
if (section.skippable && answer.toLowerCase() === "skip") {
|
|
6125
|
+
process.stdout.write("Skipped.\n\n");
|
|
6126
|
+
continue;
|
|
6127
|
+
}
|
|
6128
|
+
if (!answer.trim()) {
|
|
6129
|
+
process.stdout.write("Skipped (empty).\n\n");
|
|
6130
|
+
continue;
|
|
6131
|
+
}
|
|
6132
|
+
sections.push({ heading: section.heading, content: answer });
|
|
6133
|
+
process.stdout.write("\n");
|
|
6134
|
+
}
|
|
6135
|
+
if (sections.length === 0) {
|
|
6136
|
+
process.stdout.write("No sections provided \u2014 skipping knowledge file creation.\n");
|
|
6137
|
+
return false;
|
|
6138
|
+
}
|
|
6139
|
+
const lines = ["# Copair Knowledge Base", ""];
|
|
6140
|
+
for (const { heading, content } of sections) {
|
|
6141
|
+
lines.push(heading);
|
|
6142
|
+
const contentLines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
6143
|
+
for (const line of contentLines) {
|
|
6144
|
+
lines.push(line.startsWith("-") ? line : `- ${line}`);
|
|
6145
|
+
}
|
|
6146
|
+
lines.push("");
|
|
6147
|
+
}
|
|
6148
|
+
const fileContent = lines.join("\n");
|
|
6149
|
+
process.stdout.write("\n--- Draft COPAIR_KNOWLEDGE.md ---\n\n");
|
|
6150
|
+
process.stdout.write(fileContent);
|
|
6151
|
+
process.stdout.write("\n--- End of draft ---\n\n");
|
|
6152
|
+
const write = await confirm("Write COPAIR_KNOWLEDGE.md? (Y/n) ");
|
|
6153
|
+
if (!write) {
|
|
6154
|
+
process.stdout.write("Skipped \u2014 will prompt again next session start.\n");
|
|
6155
|
+
return false;
|
|
6156
|
+
}
|
|
6157
|
+
writeFileSync8(join12(cwd, KB_FILENAME2), fileContent, {
|
|
6158
|
+
encoding: "utf8",
|
|
6159
|
+
mode: 420
|
|
6160
|
+
});
|
|
6161
|
+
process.stdout.write(
|
|
6162
|
+
`
|
|
6163
|
+
Wrote ${KB_FILENAME2}. Commit it to version control like README.md.
|
|
6164
|
+
|
|
6165
|
+
`
|
|
6166
|
+
);
|
|
6167
|
+
return true;
|
|
6168
|
+
}
|
|
6169
|
+
};
|
|
6170
|
+
|
|
6171
|
+
// src/utils/environmentUtils.ts
|
|
6172
|
+
function isCI() {
|
|
6173
|
+
return !process.stdin.isTTY || !!process.env["CI"] || process.env["COPAIR_CI"] === "1";
|
|
6174
|
+
}
|
|
6175
|
+
|
|
5371
6176
|
// src/index.ts
|
|
5372
6177
|
function resolveModel(config, modelOverride) {
|
|
5373
6178
|
const modelAlias = modelOverride ?? config.default_model;
|
|
@@ -5421,6 +6226,18 @@ Continue from where we left off.`
|
|
|
5421
6226
|
async function main() {
|
|
5422
6227
|
const cliOpts = parseArgs();
|
|
5423
6228
|
checkForUpdates();
|
|
6229
|
+
const ci = isCI();
|
|
6230
|
+
const cwd = process.cwd();
|
|
6231
|
+
const globalInitManager = new GlobalInitManager();
|
|
6232
|
+
await globalInitManager.check({ ci });
|
|
6233
|
+
const projectInitManager = new ProjectInitManager();
|
|
6234
|
+
const projectInit = await projectInitManager.check(cwd, { ci });
|
|
6235
|
+
if (projectInit.declined) {
|
|
6236
|
+
console.log(DECLINED_MESSAGE);
|
|
6237
|
+
process.exit(0);
|
|
6238
|
+
}
|
|
6239
|
+
const gitignoreManager = new GitignoreManager();
|
|
6240
|
+
await gitignoreManager.ensureCovered(cwd, { ci });
|
|
5424
6241
|
const config = loadConfig();
|
|
5425
6242
|
const { providerName, modelAlias, providerConfig } = resolveModel(
|
|
5426
6243
|
config,
|
|
@@ -5453,50 +6270,45 @@ async function main() {
|
|
|
5453
6270
|
}
|
|
5454
6271
|
});
|
|
5455
6272
|
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
6273
|
+
gate.addTrustedPath(join13(cwd, ".copair"));
|
|
6274
|
+
const gitCtx = detectGitContext(cwd);
|
|
6275
|
+
const knowledgeManager = new KnowledgeManager({
|
|
6276
|
+
warn_size_kb: config.knowledge.warn_size_kb,
|
|
6277
|
+
max_size_kb: config.knowledge.max_size_kb
|
|
6278
|
+
});
|
|
6279
|
+
const knowledgeResult = knowledgeManager.load(cwd);
|
|
6280
|
+
let knowledgePrefix = "";
|
|
6281
|
+
if (knowledgeResult.found && knowledgeResult.content) {
|
|
6282
|
+
knowledgeManager.checkSizeBudget(knowledgeResult.sizeBytes);
|
|
6283
|
+
knowledgePrefix = knowledgeManager.injectIntoSystemPrompt(knowledgeResult.content);
|
|
6284
|
+
} else if (!ci) {
|
|
6285
|
+
const setupFlow = new KnowledgeSetupFlow();
|
|
6286
|
+
const written = await setupFlow.run(cwd);
|
|
6287
|
+
if (written) {
|
|
6288
|
+
const refreshed = knowledgeManager.load(cwd);
|
|
6289
|
+
if (refreshed.found && refreshed.content) {
|
|
6290
|
+
knowledgeManager.checkSizeBudget(refreshed.sizeBytes);
|
|
6291
|
+
knowledgePrefix = knowledgeManager.injectIntoSystemPrompt(refreshed.content);
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
5459
6294
|
}
|
|
5460
|
-
|
|
5461
|
-
gate.addTrustedPath(join9(process.cwd(), ".copair.yaml"));
|
|
5462
|
-
const gitCtx = detectGitContext(process.cwd());
|
|
5463
|
-
const knowledgeBase = new KnowledgeBase(process.cwd(), config.context.knowledge_max_size);
|
|
6295
|
+
const knowledgeBase = new KnowledgeBase(cwd, config.context.knowledge_max_size);
|
|
5464
6296
|
setKnowledgeBase(knowledgeBase);
|
|
5465
|
-
const kbSection = knowledgeBase.getSystemPromptSection();
|
|
5466
6297
|
const agent = new Agent(provider, modelAlias, toolRegistry, executor, {
|
|
5467
6298
|
bridge: agentBridge,
|
|
5468
6299
|
systemPrompt: `You are Copair, an AI coding assistant.
|
|
5469
6300
|
|
|
5470
6301
|
Environment:
|
|
5471
|
-
- Working directory: ${
|
|
5472
|
-
- All file paths MUST be absolute (start with ${
|
|
5473
|
-
|
|
5474
|
-
Context awareness:
|
|
5475
|
-
- Your context includes this system prompt, the full conversation history (all prior messages in this session), and any project knowledge shown below.
|
|
5476
|
-
- When asked about context, awareness, or what you know \u2014 answer from the conversation history and the knowledge section below. Do NOT read COPAIR_KNOWLEDGE.md or any other file to answer meta-questions about your own state.
|
|
5477
|
-
- The knowledge base below (if present) was already loaded from COPAIR_KNOWLEDGE.md at startup. Use the update_knowledge tool only to ADD new entries, not to read existing ones.
|
|
5478
|
-
|
|
5479
|
-
Rules:
|
|
5480
|
-
- You MUST use tools to perform actions. NEVER describe or narrate actions \u2014 execute them.
|
|
5481
|
-
- NEVER simulate, roleplay, or pretend to run commands. If you need to do something, call the tool.
|
|
5482
|
-
- Be brief. No preamble, no filler. No summaries between steps.
|
|
5483
|
-
- If a tool returns an error, adjust your approach \u2014 do NOT repeat the same call.
|
|
5484
|
-
|
|
5485
|
-
Work habits:
|
|
5486
|
-
- Read before editing. Keep changes minimal.
|
|
5487
|
-
- Auto-commit each discrete feature, fix, or refactor. Do not batch unrelated changes.
|
|
5488
|
-
- When you learn something project-specific (conventions, patterns, architectural decisions), use the update_knowledge tool to record it.
|
|
6302
|
+
- Working directory: ${cwd}
|
|
6303
|
+
- All file paths MUST be absolute (start with ${cwd}/)
|
|
5489
6304
|
|
|
5490
|
-
|
|
5491
|
-
- Branches: <type>/<kebab-desc> (feat, fix, chore, docs, refactor, test, perf)
|
|
5492
|
-
- Commits: <type>(<scope>): <imperative subject, max 72 chars>
|
|
5493
|
-
Body: 2-3 concise bullets. Co-authored-by is auto-appended.
|
|
5494
|
-
- NEVER use --no-verify, --force, or --no-gpg-sign.` + kbSection
|
|
6305
|
+
` + // [2] Knowledge block — injected before file context
|
|
6306
|
+
knowledgePrefix + "Context awareness:\n- Your context includes this system prompt, the full conversation history (all prior messages in this session), and any project knowledge shown above in <knowledge> tags.\n- When asked about context, awareness, or what you know \u2014 answer from the conversation history and the knowledge section. Do NOT read COPAIR_KNOWLEDGE.md to answer meta-questions about your own state.\n- COPAIR_KNOWLEDGE.md is a navigation map, not a context dump. Never write ephemeral notes or session context into it. Propose targeted diffs only when structure, conventions, or entry points change.\n\nRules:\n- You MUST use tools to perform actions. NEVER describe or narrate actions \u2014 execute them.\n- NEVER simulate, roleplay, or pretend to run commands. If you need to do something, call the tool.\n- Be brief. No preamble, no filler. No summaries between steps.\n- If a tool returns an error, adjust your approach \u2014 do NOT repeat the same call.\n\nWork habits:\n- Read before editing. Keep changes minimal.\n- Auto-commit each discrete feature, fix, or refactor. Do not batch unrelated changes.\n\nGit:\n- Branches: <type>/<kebab-desc> (feat, fix, chore, docs, refactor, test, perf)\n- Commits: <type>(<scope>): <imperative subject, max 72 chars>\n Body: 2-3 concise bullets. Co-authored-by is auto-appended.\n- NEVER use --no-verify, --force, or --no-gpg-sign."
|
|
5495
6307
|
});
|
|
5496
|
-
const sessionManager = new SessionManager(
|
|
5497
|
-
const sessionsDir = resolveSessionsDir(
|
|
5498
|
-
warnIfSessionsTracked(
|
|
5499
|
-
await SessionManager.migrateGlobalRecovery(sessionsDir,
|
|
6308
|
+
const sessionManager = new SessionManager(cwd);
|
|
6309
|
+
const sessionsDir = resolveSessionsDir(cwd);
|
|
6310
|
+
warnIfSessionsTracked(cwd);
|
|
6311
|
+
await SessionManager.migrateGlobalRecovery(sessionsDir, cwd);
|
|
5500
6312
|
await SessionManager.cleanup(sessionsDir, config.context.max_sessions);
|
|
5501
6313
|
let sessionResumed = false;
|
|
5502
6314
|
const sessions = await SessionManager.listSessions(sessionsDir);
|
|
@@ -5531,14 +6343,14 @@ Git:
|
|
|
5531
6343
|
let identifierDerived = sessionResumed;
|
|
5532
6344
|
setSessionManagerRef(sessionManager);
|
|
5533
6345
|
const agentContext = {
|
|
5534
|
-
cwd
|
|
6346
|
+
cwd,
|
|
5535
6347
|
model: modelAlias,
|
|
5536
6348
|
branch: gitCtx.branch
|
|
5537
6349
|
};
|
|
5538
6350
|
const cmdRegistry = new CommandRegistry();
|
|
5539
6351
|
const workflowCmd = createWorkflowCommand(
|
|
5540
|
-
async (
|
|
5541
|
-
await agent.handleMessage(
|
|
6352
|
+
async (prompt4) => {
|
|
6353
|
+
await agent.handleMessage(prompt4);
|
|
5542
6354
|
},
|
|
5543
6355
|
async (input) => {
|
|
5544
6356
|
const result = await cmdRegistry.execute(input, { ...agentContext, model: agent.model });
|
|
@@ -5554,7 +6366,7 @@ Git:
|
|
|
5554
6366
|
workflowCmd
|
|
5555
6367
|
);
|
|
5556
6368
|
const tokenTracker = new TokenTracker(DEFAULT_PRICING);
|
|
5557
|
-
const historyPath = resolveHistoryPath(
|
|
6369
|
+
const historyPath = resolveHistoryPath(cwd);
|
|
5558
6370
|
const inputHistory = loadHistory(historyPath);
|
|
5559
6371
|
const completionEngine = new CompletionEngine();
|
|
5560
6372
|
const cmdNames = /* @__PURE__ */ new Map();
|
|
@@ -5567,7 +6379,7 @@ Git:
|
|
|
5567
6379
|
cmdNames.set("clear", "Clear conversation");
|
|
5568
6380
|
cmdNames.set("model", "Switch model");
|
|
5569
6381
|
completionEngine.addProvider(new SlashCommandProvider(cmdNames));
|
|
5570
|
-
completionEngine.addProvider(new FilePathProvider(
|
|
6382
|
+
completionEngine.addProvider(new FilePathProvider(cwd));
|
|
5571
6383
|
printBanner(modelAlias);
|
|
5572
6384
|
await new Promise((r) => setTimeout(r, 50));
|
|
5573
6385
|
let appHandle = null;
|