@harperfast/agent 0.10.6
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 +172 -0
- package/README.svg +78 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +2917 -0
- package/package.json +82 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,2917 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// agent.ts
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import { Agent as Agent3, run as run2 } from "@openai/agents";
|
|
6
|
+
import chalk13 from "chalk";
|
|
7
|
+
|
|
8
|
+
// lifecycle/cleanUpAndSayBye.ts
|
|
9
|
+
import { getGlobalTraceProvider } from "@openai/agents";
|
|
10
|
+
|
|
11
|
+
// utils/sessions/cost.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
var FLEX_PRICES = {
|
|
14
|
+
"gpt-5.2": { input: 0.875 / 1e6, cachedInput: 0.0875 / 1e6, output: 7 / 1e6 },
|
|
15
|
+
"gpt-5.1": { input: 0.625 / 1e6, cachedInput: 0.0625 / 1e6, output: 5 / 1e6 },
|
|
16
|
+
"gpt-5": { input: 0.625 / 1e6, cachedInput: 0.0625 / 1e6, output: 5 / 1e6 },
|
|
17
|
+
"gpt-5-mini": { input: 0.125 / 1e6, cachedInput: 0.0125 / 1e6, output: 1 / 1e6 },
|
|
18
|
+
"gpt-5-nano": { input: 0.025 / 1e6, cachedInput: 25e-4 / 1e6, output: 0.2 / 1e6 },
|
|
19
|
+
"o3": { input: 1 / 1e6, cachedInput: 0.25 / 1e6, output: 4 / 1e6 },
|
|
20
|
+
"o4-mini": { input: 0.55 / 1e6, cachedInput: 0.138 / 1e6, output: 2.2 / 1e6 }
|
|
21
|
+
};
|
|
22
|
+
var STANDARD_PRICES = {
|
|
23
|
+
"gpt-5.2": { input: 1.75 / 1e6, cachedInput: 0.175 / 1e6, output: 14 / 1e6 },
|
|
24
|
+
"gpt-5.1": { input: 1.25 / 1e6, cachedInput: 0.125 / 1e6, output: 10 / 1e6 },
|
|
25
|
+
"gpt-5": { input: 1.25 / 1e6, cachedInput: 0.125 / 1e6, output: 10 / 1e6 },
|
|
26
|
+
"gpt-5-mini": { input: 0.25 / 1e6, cachedInput: 0.025 / 1e6, output: 2 / 1e6 },
|
|
27
|
+
"gpt-5-nano": { input: 0.05 / 1e6, cachedInput: 5e-3 / 1e6, output: 0.4 / 1e6 },
|
|
28
|
+
"gpt-5.2-chat-latest": { input: 1.75 / 1e6, cachedInput: 0.175 / 1e6, output: 14 / 1e6 },
|
|
29
|
+
"gpt-5.1-chat-latest": { input: 1.25 / 1e6, cachedInput: 0.125 / 1e6, output: 10 / 1e6 },
|
|
30
|
+
"gpt-5-chat-latest": { input: 1.25 / 1e6, cachedInput: 0.125 / 1e6, output: 10 / 1e6 },
|
|
31
|
+
"gpt-5.2-pro": { input: 21 / 1e6, cachedInput: 21 / 1e6, output: 168 / 1e6 },
|
|
32
|
+
"gpt-5-pro": { input: 15 / 1e6, cachedInput: 15 / 1e6, output: 120 / 1e6 },
|
|
33
|
+
"gpt-4.1": { input: 2 / 1e6, cachedInput: 0.5 / 1e6, output: 8 / 1e6 },
|
|
34
|
+
"gpt-4.1-mini": { input: 0.4 / 1e6, cachedInput: 0.1 / 1e6, output: 1.6 / 1e6 },
|
|
35
|
+
"gpt-4.1-nano": { input: 0.1 / 1e6, cachedInput: 0.025 / 1e6, output: 0.4 / 1e6 },
|
|
36
|
+
"gpt-4o": { input: 2.5 / 1e6, cachedInput: 1.25 / 1e6, output: 10 / 1e6 },
|
|
37
|
+
"gpt-4o-2024-05-13": { input: 5 / 1e6, cachedInput: 5 / 1e6, output: 15 / 1e6 },
|
|
38
|
+
"gpt-4o-mini": { input: 0.15 / 1e6, cachedInput: 0.075 / 1e6, output: 0.6 / 1e6 },
|
|
39
|
+
"o1": { input: 15 / 1e6, cachedInput: 7.5 / 1e6, output: 60 / 1e6 },
|
|
40
|
+
"o1-pro": { input: 150 / 1e6, cachedInput: 150 / 1e6, output: 600 / 1e6 },
|
|
41
|
+
"o3-pro": { input: 20 / 1e6, cachedInput: 20 / 1e6, output: 80 / 1e6 },
|
|
42
|
+
"o3": { input: 2 / 1e6, cachedInput: 0.5 / 1e6, output: 8 / 1e6 },
|
|
43
|
+
"o3-deep-research": { input: 10 / 1e6, cachedInput: 2.5 / 1e6, output: 40 / 1e6 },
|
|
44
|
+
"o4-mini": { input: 1.1 / 1e6, cachedInput: 0.275 / 1e6, output: 4.4 / 1e6 },
|
|
45
|
+
"o4-mini-deep-research": { input: 2 / 1e6, cachedInput: 0.5 / 1e6, output: 8 / 1e6 },
|
|
46
|
+
"o3-mini": { input: 1.1 / 1e6, cachedInput: 0.55 / 1e6, output: 4.4 / 1e6 },
|
|
47
|
+
"o1-mini": { input: 1.1 / 1e6, cachedInput: 0.55 / 1e6, output: 4.4 / 1e6 }
|
|
48
|
+
};
|
|
49
|
+
var MODEL_PRICES = {
|
|
50
|
+
flex: FLEX_PRICES,
|
|
51
|
+
standard: STANDARD_PRICES
|
|
52
|
+
};
|
|
53
|
+
function hasKnownPrices(model, tier = "flex") {
|
|
54
|
+
return !!model && model in MODEL_PRICES[tier];
|
|
55
|
+
}
|
|
56
|
+
function extractCachedTokens(inputTokenDetails) {
|
|
57
|
+
let cachedInputTokens = 0;
|
|
58
|
+
if (inputTokenDetails) {
|
|
59
|
+
const details = Array.isArray(inputTokenDetails) ? inputTokenDetails : [inputTokenDetails];
|
|
60
|
+
for (const inputTokenDetail of details) {
|
|
61
|
+
if (inputTokenDetail["cached_tokens"]) {
|
|
62
|
+
cachedInputTokens += inputTokenDetail["cached_tokens"];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return cachedInputTokens;
|
|
67
|
+
}
|
|
68
|
+
function calculateCost(model, inputTokens, outputTokens, inputTokenDetails, tier = "flex") {
|
|
69
|
+
if (!model) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const prices = MODEL_PRICES[tier][model];
|
|
73
|
+
if (!prices) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const cachedInputTokens = extractCachedTokens(inputTokenDetails);
|
|
77
|
+
const fullRateInputTokens = inputTokens - cachedInputTokens;
|
|
78
|
+
return fullRateInputTokens * prices.input + cachedInputTokens * prices.cachedInput + outputTokens * prices.output;
|
|
79
|
+
}
|
|
80
|
+
var CostTracker = class {
|
|
81
|
+
totalInputTokens = 0;
|
|
82
|
+
totalCachedInputTokens = 0;
|
|
83
|
+
totalOutputTokens = 0;
|
|
84
|
+
totalCost = 0;
|
|
85
|
+
totalCompactionCost = 0;
|
|
86
|
+
hasUnknownPrices = false;
|
|
87
|
+
reset() {
|
|
88
|
+
this.totalInputTokens = 0;
|
|
89
|
+
this.totalCachedInputTokens = 0;
|
|
90
|
+
this.totalOutputTokens = 0;
|
|
91
|
+
this.totalCost = 0;
|
|
92
|
+
this.totalCompactionCost = 0;
|
|
93
|
+
this.hasUnknownPrices = false;
|
|
94
|
+
}
|
|
95
|
+
recordTurn(model, usage, compactionModel) {
|
|
96
|
+
const { turnCost, compactionCost, unknownPrices } = this.calculateUsageCosts(model, usage, compactionModel);
|
|
97
|
+
this.totalInputTokens += usage.inputTokens;
|
|
98
|
+
this.totalCachedInputTokens += extractCachedTokens(usage.inputTokensDetails);
|
|
99
|
+
this.totalOutputTokens += usage.outputTokens;
|
|
100
|
+
this.totalCost += turnCost;
|
|
101
|
+
this.totalCompactionCost += compactionCost;
|
|
102
|
+
if (unknownPrices) {
|
|
103
|
+
this.hasUnknownPrices = true;
|
|
104
|
+
}
|
|
105
|
+
return turnCost + compactionCost;
|
|
106
|
+
}
|
|
107
|
+
calculateUsageCosts(model, usage, compactionModel) {
|
|
108
|
+
let turnCost = 0;
|
|
109
|
+
let compactionCost = 0;
|
|
110
|
+
let unknownPrices = !hasKnownPrices(model, "flex");
|
|
111
|
+
if (usage?.requestUsageEntries && usage.requestUsageEntries.length > 0) {
|
|
112
|
+
for (const entry of usage.requestUsageEntries) {
|
|
113
|
+
const isCompaction = entry.endpoint === "responses.compact";
|
|
114
|
+
let tier = isCompaction ? "standard" : "flex";
|
|
115
|
+
const entryActualTier = entry.serviceTier;
|
|
116
|
+
if (entryActualTier === "flex") {
|
|
117
|
+
tier = "flex";
|
|
118
|
+
} else if (entryActualTier === "scale" || entryActualTier === "standard") {
|
|
119
|
+
tier = "standard";
|
|
120
|
+
}
|
|
121
|
+
const entryModel = isCompaction ? compactionModel : model;
|
|
122
|
+
if (!hasKnownPrices(entryModel, tier)) {
|
|
123
|
+
unknownPrices = true;
|
|
124
|
+
}
|
|
125
|
+
const entryCost = calculateCost(
|
|
126
|
+
entryModel,
|
|
127
|
+
entry.inputTokens,
|
|
128
|
+
entry.outputTokens,
|
|
129
|
+
entry.inputTokensDetails,
|
|
130
|
+
tier
|
|
131
|
+
);
|
|
132
|
+
if (isCompaction) {
|
|
133
|
+
compactionCost += entryCost;
|
|
134
|
+
} else {
|
|
135
|
+
turnCost += entryCost;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else if (usage) {
|
|
139
|
+
const usageActualTier = usage.serviceTier;
|
|
140
|
+
const tier = usageActualTier === "scale" || usageActualTier === "standard" ? "standard" : "flex";
|
|
141
|
+
turnCost = calculateCost(model, usage.inputTokens, usage.outputTokens, usage.inputTokensDetails, tier);
|
|
142
|
+
}
|
|
143
|
+
return { turnCost, compactionCost, unknownPrices };
|
|
144
|
+
}
|
|
145
|
+
getStatusString(currentTurnUsage, model, compactionModel) {
|
|
146
|
+
const { turnCost, compactionCost, unknownPrices } = this.calculateUsageCosts(
|
|
147
|
+
model,
|
|
148
|
+
currentTurnUsage,
|
|
149
|
+
compactionModel
|
|
150
|
+
);
|
|
151
|
+
if (this.hasUnknownPrices || unknownPrices) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
const totalTurnCost = turnCost + compactionCost;
|
|
155
|
+
return chalk.dim(
|
|
156
|
+
`[~${chalk.cyan("$" + (this.totalCost + this.totalCompactionCost + totalTurnCost).toFixed(4))}]`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
logFinalStats() {
|
|
160
|
+
if (this.totalInputTokens > 0 || this.totalOutputTokens > 0) {
|
|
161
|
+
const fmt = (n) => new Intl.NumberFormat().format(n);
|
|
162
|
+
const cachedStr = this.totalCachedInputTokens > 0 ? ` (${chalk.cyan(fmt(this.totalCachedInputTokens))} cached)` : "";
|
|
163
|
+
const showCost = !this.hasUnknownPrices;
|
|
164
|
+
const totalCost = this.totalCost + this.totalCompactionCost;
|
|
165
|
+
let statsStr = chalk.dim(
|
|
166
|
+
`Final session usage: ${chalk.cyan(fmt(this.totalInputTokens))} input${cachedStr}, ${chalk.cyan(fmt(this.totalOutputTokens))} output tokens.`
|
|
167
|
+
);
|
|
168
|
+
if (showCost) {
|
|
169
|
+
statsStr += chalk.dim(` Total cost: ~${chalk.cyan("$" + totalCost.toFixed(4))}`);
|
|
170
|
+
if (this.totalCompactionCost > 0) {
|
|
171
|
+
statsStr += chalk.dim(
|
|
172
|
+
` (Compaction: ${chalk.cyan("$" + this.totalCompactionCost.toFixed(4))})`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
console.log(statsStr);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var costTracker = new CostTracker();
|
|
181
|
+
|
|
182
|
+
// utils/shell/harperProcess.ts
|
|
183
|
+
import spawn from "cross-spawn";
|
|
184
|
+
import { execSync } from "child_process";
|
|
185
|
+
import { homedir } from "os";
|
|
186
|
+
import { join } from "path";
|
|
187
|
+
var HarperProcess = class {
|
|
188
|
+
childProcess = null;
|
|
189
|
+
externalPid = null;
|
|
190
|
+
logTailProcess = null;
|
|
191
|
+
logs = [];
|
|
192
|
+
httpPort = 9926;
|
|
193
|
+
get running() {
|
|
194
|
+
if (this.childProcess !== null) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
this.updateExternalStatus();
|
|
198
|
+
return this.externalPid !== null;
|
|
199
|
+
}
|
|
200
|
+
get startedInternally() {
|
|
201
|
+
return this.childProcess !== null;
|
|
202
|
+
}
|
|
203
|
+
updateExternalStatus() {
|
|
204
|
+
try {
|
|
205
|
+
const status = execSync("harper status", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
206
|
+
if (status.includes("status: running")) {
|
|
207
|
+
const pidMatch = status.match(/pid: (\d+)/);
|
|
208
|
+
if (pidMatch?.[1]) {
|
|
209
|
+
this.externalPid = parseInt(pidMatch[1], 10);
|
|
210
|
+
if (!this.logTailProcess) {
|
|
211
|
+
this.startTailingLogs();
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
this.externalPid = null;
|
|
219
|
+
this.stopTailingLogs();
|
|
220
|
+
}
|
|
221
|
+
startTailingLogs() {
|
|
222
|
+
const logPath = join(homedir(), "hdb", "log", "hdb.log");
|
|
223
|
+
this.logTailProcess = spawn("tail", ["-f", logPath], {
|
|
224
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
225
|
+
});
|
|
226
|
+
this.logTailProcess.stdout?.on("data", (data) => {
|
|
227
|
+
const string = data.toString();
|
|
228
|
+
this.updatePortFromLogs(string);
|
|
229
|
+
this.logs.push(string);
|
|
230
|
+
});
|
|
231
|
+
this.logTailProcess.stderr?.on("data", (data) => {
|
|
232
|
+
this.logs.push(data.toString());
|
|
233
|
+
});
|
|
234
|
+
this.logTailProcess.on("exit", () => {
|
|
235
|
+
this.logTailProcess = null;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
stopTailingLogs() {
|
|
239
|
+
if (this.logTailProcess) {
|
|
240
|
+
this.logTailProcess.kill();
|
|
241
|
+
this.logTailProcess = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
updatePortFromLogs(string) {
|
|
245
|
+
if (string.includes("REST:") && string.includes("HTTP:")) {
|
|
246
|
+
this.httpPort = parseInt(string.split("HTTP:").pop().split(",")[0], 10);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
start(directoryName) {
|
|
250
|
+
if (this.running) {
|
|
251
|
+
throw new Error("Harper process is already running.");
|
|
252
|
+
}
|
|
253
|
+
this.logs = [];
|
|
254
|
+
this.childProcess = spawn("harperdb", ["dev", "."], {
|
|
255
|
+
cwd: directoryName,
|
|
256
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
257
|
+
});
|
|
258
|
+
this.childProcess.stdout?.on("data", (data) => {
|
|
259
|
+
const string = data.toString();
|
|
260
|
+
this.updatePortFromLogs(string);
|
|
261
|
+
this.logs.push(string);
|
|
262
|
+
});
|
|
263
|
+
this.childProcess.stderr?.on("data", (data) => {
|
|
264
|
+
this.logs.push(data.toString());
|
|
265
|
+
});
|
|
266
|
+
this.childProcess.on("exit", () => {
|
|
267
|
+
this.childProcess = null;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
stop() {
|
|
271
|
+
if (this.childProcess) {
|
|
272
|
+
this.childProcess.kill();
|
|
273
|
+
this.childProcess = null;
|
|
274
|
+
}
|
|
275
|
+
if (this.externalPid) {
|
|
276
|
+
try {
|
|
277
|
+
process.kill(this.externalPid);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
this.externalPid = null;
|
|
281
|
+
}
|
|
282
|
+
this.stopTailingLogs();
|
|
283
|
+
}
|
|
284
|
+
getAndClearLogs() {
|
|
285
|
+
const logs = this.logs;
|
|
286
|
+
this.logs = [];
|
|
287
|
+
return logs.join("");
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var harperProcess = new HarperProcess();
|
|
291
|
+
|
|
292
|
+
// utils/shell/harperResponse.ts
|
|
293
|
+
import chalk2 from "chalk";
|
|
294
|
+
function harperResponse(text) {
|
|
295
|
+
if (text) {
|
|
296
|
+
console.log(`${chalk2.bold("Harper:")} ${chalk2.cyan(text)}
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// utils/shell/spinner.ts
|
|
302
|
+
import process2 from "process";
|
|
303
|
+
|
|
304
|
+
// lifecycle/trackedState.ts
|
|
305
|
+
import "@openai/agents";
|
|
306
|
+
var trackedState = {
|
|
307
|
+
agent: null,
|
|
308
|
+
cwd: process.cwd(),
|
|
309
|
+
atStartOfLine: true,
|
|
310
|
+
emptyLines: 0,
|
|
311
|
+
approvalState: null,
|
|
312
|
+
controller: null,
|
|
313
|
+
model: "",
|
|
314
|
+
compactionModel: "",
|
|
315
|
+
sessionPath: null,
|
|
316
|
+
useFlexTier: false,
|
|
317
|
+
disableSpinner: false,
|
|
318
|
+
enableInterruptions: true
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// utils/shell/spinner.ts
|
|
322
|
+
var Spinner = class {
|
|
323
|
+
interval = null;
|
|
324
|
+
chars = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
325
|
+
i = 0;
|
|
326
|
+
message;
|
|
327
|
+
status = "";
|
|
328
|
+
onData = null;
|
|
329
|
+
constructor(message = "Thinking...") {
|
|
330
|
+
this.message = message;
|
|
331
|
+
}
|
|
332
|
+
get isSpinning() {
|
|
333
|
+
return this.interval !== null;
|
|
334
|
+
}
|
|
335
|
+
start() {
|
|
336
|
+
const disabled = trackedState.disableSpinner || process2.env.HARPER_AGENT_NO_SPINNER === "true" || process2.env.HARPER_AGENT_NO_SPINNER === "1" || process2.env.HARPER_AGENT_DISABLE_SPINNER === "true" || process2.env.HARPER_AGENT_DISABLE_SPINNER === "1";
|
|
337
|
+
if (this.interval) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (trackedState.enableInterruptions) {
|
|
341
|
+
this.onData = (data) => {
|
|
342
|
+
const str = data.toString();
|
|
343
|
+
if (str.includes("\n") || str.includes("\r")) {
|
|
344
|
+
this.interrupt();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
process2.stdin.on("data", this.onData);
|
|
348
|
+
if (process2.stdin.isTTY) {
|
|
349
|
+
process2.stdin.resume();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (disabled) {
|
|
353
|
+
this.interval = null;
|
|
354
|
+
console.log("Thinking...");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.i = 0;
|
|
358
|
+
process2.stdout.write(`${this.chars[this.i]} ${this.message}${this.status ? " " + this.status : ""}\x1B[K`);
|
|
359
|
+
this.interval = setInterval(() => {
|
|
360
|
+
this.i = (this.i + 1) % this.chars.length;
|
|
361
|
+
process2.stdout.write(`\r${this.chars[this.i]} ${this.message}${this.status ? " " + this.status : ""}\x1B[K`);
|
|
362
|
+
}, 80);
|
|
363
|
+
}
|
|
364
|
+
interrupt() {
|
|
365
|
+
if (!trackedState.enableInterruptions) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (trackedState.controller) {
|
|
369
|
+
this.stop();
|
|
370
|
+
trackedState.controller.abort();
|
|
371
|
+
harperResponse("<thought interrupted>");
|
|
372
|
+
trackedState.atStartOfLine = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
stop() {
|
|
376
|
+
if (this.onData) {
|
|
377
|
+
process2.stdin.removeListener("data", this.onData);
|
|
378
|
+
this.onData = null;
|
|
379
|
+
}
|
|
380
|
+
if (this.interval) {
|
|
381
|
+
clearInterval(this.interval);
|
|
382
|
+
this.interval = null;
|
|
383
|
+
this.status = "";
|
|
384
|
+
process2.stdout.write("\r\x1B[K");
|
|
385
|
+
} else {
|
|
386
|
+
this.status = "";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var spinner = new Spinner();
|
|
391
|
+
|
|
392
|
+
// lifecycle/cleanUpAndSayBye.ts
|
|
393
|
+
async function cleanUpAndSayBye() {
|
|
394
|
+
spinner.stop();
|
|
395
|
+
costTracker.logFinalStats();
|
|
396
|
+
if (harperProcess.startedInternally) {
|
|
397
|
+
harperProcess.stop();
|
|
398
|
+
}
|
|
399
|
+
console.log("");
|
|
400
|
+
harperResponse("See you later!");
|
|
401
|
+
await getGlobalTraceProvider().forceFlush();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// lifecycle/defaultInstructions.ts
|
|
405
|
+
import { existsSync } from "fs";
|
|
406
|
+
import { join as join2 } from "path";
|
|
407
|
+
function defaultInstructions() {
|
|
408
|
+
const harperAppExists = existsSync(join2(trackedState.cwd, "config.yaml"));
|
|
409
|
+
const vibing = harperAppExists ? "updating" : "creating";
|
|
410
|
+
return `You are working on ${vibing} a harper app with the user.`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// lifecycle/getModel.ts
|
|
414
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
415
|
+
import { google } from "@ai-sdk/google";
|
|
416
|
+
import { openai } from "@ai-sdk/openai";
|
|
417
|
+
import { aisdk } from "@openai/agents-extensions";
|
|
418
|
+
import "@openai/agents-extensions/ai-sdk";
|
|
419
|
+
import { createOllama, ollama } from "ollama-ai-provider-v2";
|
|
420
|
+
function isOpenAIModel(modelName) {
|
|
421
|
+
if (!modelName || modelName === "gpt-5.2") {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
return !modelName.startsWith("claude-") && !modelName.startsWith("gemini-") && !modelName.startsWith("ollama-");
|
|
425
|
+
}
|
|
426
|
+
function getModel(modelName) {
|
|
427
|
+
if (modelName.startsWith("claude-")) {
|
|
428
|
+
return aisdk(anthropic(modelName));
|
|
429
|
+
}
|
|
430
|
+
if (modelName.startsWith("gemini-")) {
|
|
431
|
+
return aisdk(google(modelName));
|
|
432
|
+
}
|
|
433
|
+
if (modelName.startsWith("ollama-")) {
|
|
434
|
+
const ollamaBaseUrl = process.env.OLLAMA_BASE_URL ? normalizeOllamaBaseUrl(process.env.OLLAMA_BASE_URL) : void 0;
|
|
435
|
+
const ollamaProvider = ollamaBaseUrl ? createOllama({ baseURL: ollamaBaseUrl }) : ollama;
|
|
436
|
+
return aisdk(ollamaProvider(getModelName(modelName)));
|
|
437
|
+
}
|
|
438
|
+
return aisdk(openai(modelName));
|
|
439
|
+
}
|
|
440
|
+
function getModelName(modelName) {
|
|
441
|
+
if (modelName.startsWith("claude-")) {
|
|
442
|
+
return modelName;
|
|
443
|
+
}
|
|
444
|
+
if (modelName.startsWith("gemini-")) {
|
|
445
|
+
return modelName;
|
|
446
|
+
}
|
|
447
|
+
if (modelName.startsWith("ollama-")) {
|
|
448
|
+
return modelName.replace("ollama-", "");
|
|
449
|
+
}
|
|
450
|
+
return modelName;
|
|
451
|
+
}
|
|
452
|
+
function normalizeOllamaBaseUrl(baseUrl) {
|
|
453
|
+
let url = baseUrl.trim();
|
|
454
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
455
|
+
url = `http://${url}`;
|
|
456
|
+
}
|
|
457
|
+
const urlObj = new URL(url);
|
|
458
|
+
if (!urlObj.port) {
|
|
459
|
+
urlObj.port = "11434";
|
|
460
|
+
}
|
|
461
|
+
let pathname = urlObj.pathname;
|
|
462
|
+
if (pathname.endsWith("/")) {
|
|
463
|
+
pathname = pathname.slice(0, -1);
|
|
464
|
+
}
|
|
465
|
+
if (!pathname.endsWith("/api")) {
|
|
466
|
+
pathname += "/api";
|
|
467
|
+
}
|
|
468
|
+
urlObj.pathname = pathname;
|
|
469
|
+
return urlObj.toString().replace(/\/$/, "");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// utils/shell/cli.ts
|
|
473
|
+
import chalk3 from "chalk";
|
|
474
|
+
|
|
475
|
+
// utils/package/getOwnPackageJson.ts
|
|
476
|
+
import { readFileSync } from "fs";
|
|
477
|
+
import { join as join3 } from "path";
|
|
478
|
+
import { fileURLToPath } from "url";
|
|
479
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
480
|
+
function getOwnPackageJson() {
|
|
481
|
+
try {
|
|
482
|
+
const packageContents = readFileSync(join3(__dirname, "../package.json"), "utf8");
|
|
483
|
+
return JSON.parse(packageContents);
|
|
484
|
+
} catch {
|
|
485
|
+
return { name: "@harperfast/agent", version: "0.0.0" };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// utils/shell/cli.ts
|
|
490
|
+
function isHelpRequest(args) {
|
|
491
|
+
const helpVariants = ["--help", "-h", "help"];
|
|
492
|
+
return args.some((arg) => helpVariants.includes(arg.toLowerCase()));
|
|
493
|
+
}
|
|
494
|
+
function isVersionRequest(args) {
|
|
495
|
+
const versionVariants = ["--version", "-v", "version"];
|
|
496
|
+
return args.some((arg) => versionVariants.includes(arg.toLowerCase()));
|
|
497
|
+
}
|
|
498
|
+
function handleHelp() {
|
|
499
|
+
console.log(`
|
|
500
|
+
${chalk3.bold("harper-agent")} - AI to help you with Harper app management
|
|
501
|
+
|
|
502
|
+
${chalk3.bold("USAGE")}
|
|
503
|
+
$ harper-agent [options]
|
|
504
|
+
$ harper-agent [command]
|
|
505
|
+
|
|
506
|
+
${chalk3.bold("OPTIONS")}
|
|
507
|
+
-h, --help Show help information
|
|
508
|
+
-v, --version Show version information
|
|
509
|
+
-m, --model Specify the model to use (e.g., gpt-4o, claude-3-5-sonnet, ollama-llama3)
|
|
510
|
+
Can also be set via HARPER_AGENT_MODEL environment variable.
|
|
511
|
+
For Ollama, use the ollama- prefix (e.g., ollama-llama3).
|
|
512
|
+
-c, --compaction-model Specify the compaction model to use (defaults to gpt-4o-mini).
|
|
513
|
+
Can also be set via HARPER_AGENT_COMPACTION_MODEL environment variable.
|
|
514
|
+
-s, --session Specify a path to a SQLite database file to persist the chat session.
|
|
515
|
+
Can also be set via HARPER_AGENT_SESSION environment variable.
|
|
516
|
+
--flex-tier Force the use of the flex service tier for lower costs but potentially
|
|
517
|
+
more errors under high system load.
|
|
518
|
+
Can also be set via HARPER_AGENT_FLEX_TIER=true environment variable.
|
|
519
|
+
--no-spinner Disable the thinking spinner (also: --disable-spinner)
|
|
520
|
+
Can also be set via HARPER_AGENT_NO_SPINNER=1 or HARPER_AGENT_DISABLE_SPINNER=1.
|
|
521
|
+
--no-interruptions Disable stdin-based interruption logic
|
|
522
|
+
Aliases: --no-interrupt, --no-interrupts, --disable-interrupt,
|
|
523
|
+
--disable-interrupts, --disable-interruptions
|
|
524
|
+
Env: set HARPER_AGENT_DISABLE_INTERRUPTION=1 or HARPER_AGENT_DISABLE_INTERRUPTIONS=1
|
|
525
|
+
(default interruptions are enabled). You can also set
|
|
526
|
+
HARPER_AGENT_ENABLE_INTERRUPTION=0 or HARPER_AGENT_ENABLE_INTERRUPTIONS=0.
|
|
527
|
+
|
|
528
|
+
${chalk3.bold("COMMANDS")}
|
|
529
|
+
--help Show help information
|
|
530
|
+
--version Show version information
|
|
531
|
+
|
|
532
|
+
${chalk3.bold("EXAMPLES")}
|
|
533
|
+
$ harper-agent --help
|
|
534
|
+
$ harper-agent --version
|
|
535
|
+
$ harper-agent
|
|
536
|
+
`);
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
function handleVersion() {
|
|
540
|
+
const pkg = getOwnPackageJson();
|
|
541
|
+
console.log(pkg.version);
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// utils/strings/isFalse.ts
|
|
546
|
+
function isFalse(v) {
|
|
547
|
+
if (v === void 0) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
const val = v.trim().toLowerCase();
|
|
551
|
+
return val === "false" || val === "0" || val === "no" || val === "off";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// utils/strings/isTrue.ts
|
|
555
|
+
function isTrue(v) {
|
|
556
|
+
if (v === void 0) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
const val = v.trim().toLowerCase();
|
|
560
|
+
return val === "true" || val === "1" || val === "yes" || val === "on";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// lifecycle/parseArgs.ts
|
|
564
|
+
function stripQuotes(str) {
|
|
565
|
+
if (str.startsWith('"') && str.endsWith('"') || str.startsWith("'") && str.endsWith("'")) {
|
|
566
|
+
return str.slice(1, -1);
|
|
567
|
+
}
|
|
568
|
+
return str;
|
|
569
|
+
}
|
|
570
|
+
function parseArgs() {
|
|
571
|
+
const args = process.argv.slice(2);
|
|
572
|
+
if (isHelpRequest(args)) {
|
|
573
|
+
handleHelp();
|
|
574
|
+
}
|
|
575
|
+
if (isVersionRequest(args)) {
|
|
576
|
+
handleVersion();
|
|
577
|
+
}
|
|
578
|
+
for (let i = 0; i < args.length; i++) {
|
|
579
|
+
const arg = args[i];
|
|
580
|
+
const flagPairs = [
|
|
581
|
+
["model", ["--model", "-m", "model"]],
|
|
582
|
+
["compactionModel", ["--compaction-model", "-c", "compaction-model"]],
|
|
583
|
+
["sessionPath", ["--session", "-s", "session"]]
|
|
584
|
+
];
|
|
585
|
+
let handled = false;
|
|
586
|
+
for (const [key, prefixes] of flagPairs) {
|
|
587
|
+
for (const prefix of prefixes) {
|
|
588
|
+
if (arg === prefix) {
|
|
589
|
+
if (args[i + 1]) {
|
|
590
|
+
trackedState[key] = stripQuotes(args[++i]);
|
|
591
|
+
}
|
|
592
|
+
handled = true;
|
|
593
|
+
break;
|
|
594
|
+
} else if (arg.startsWith(`${prefix}=`)) {
|
|
595
|
+
trackedState[key] = stripQuotes(arg.slice(prefix.length + 1));
|
|
596
|
+
handled = true;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (handled) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (handled) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (arg === "--flex-tier") {
|
|
608
|
+
trackedState.useFlexTier = true;
|
|
609
|
+
} else if (arg === "--no-spinner" || arg === "--disable-spinner") {
|
|
610
|
+
trackedState.disableSpinner = true;
|
|
611
|
+
} else if ([
|
|
612
|
+
"--no-interrupt",
|
|
613
|
+
"--no-interrupts",
|
|
614
|
+
"--no-interruptions",
|
|
615
|
+
"--disable-interrupt",
|
|
616
|
+
"--disable-interrupts",
|
|
617
|
+
"--disable-interruptions"
|
|
618
|
+
].includes(arg)) {
|
|
619
|
+
trackedState.enableInterruptions = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (!trackedState.model && process.env.HARPER_AGENT_MODEL) {
|
|
623
|
+
trackedState.model = process.env.HARPER_AGENT_MODEL;
|
|
624
|
+
}
|
|
625
|
+
if (!trackedState.compactionModel && process.env.HARPER_AGENT_COMPACTION_MODEL) {
|
|
626
|
+
trackedState.compactionModel = process.env.HARPER_AGENT_COMPACTION_MODEL;
|
|
627
|
+
}
|
|
628
|
+
if (!trackedState.sessionPath && process.env.HARPER_AGENT_SESSION) {
|
|
629
|
+
trackedState.sessionPath = process.env.HARPER_AGENT_SESSION;
|
|
630
|
+
}
|
|
631
|
+
if (!trackedState.useFlexTier && isTrue(process.env.HARPER_AGENT_FLEX_TIER)) {
|
|
632
|
+
trackedState.useFlexTier = true;
|
|
633
|
+
}
|
|
634
|
+
if (!trackedState.disableSpinner && (isTrue(process.env.HARPER_AGENT_NO_SPINNER) || isTrue(process.env.HARPER_AGENT_DISABLE_SPINNER))) {
|
|
635
|
+
trackedState.disableSpinner = true;
|
|
636
|
+
}
|
|
637
|
+
if (isTrue(process.env.HARPER_AGENT_DISABLE_INTERRUPTION) || isTrue(process.env.HARPER_AGENT_DISABLE_INTERRUPTIONS) || isFalse(process.env.HARPER_AGENT_ENABLE_INTERRUPTION) || isFalse(process.env.HARPER_AGENT_ENABLE_INTERRUPTIONS)) {
|
|
638
|
+
trackedState.enableInterruptions = false;
|
|
639
|
+
}
|
|
640
|
+
if (!trackedState.model) {
|
|
641
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
642
|
+
trackedState.model = "claude-3-7-sonnet-latest";
|
|
643
|
+
} else if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
|
644
|
+
trackedState.model = "gemini-2.0-flash";
|
|
645
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
646
|
+
trackedState.model = "gpt-5.2";
|
|
647
|
+
} else if (process.env.OLLAMA_BASE_URL) {
|
|
648
|
+
trackedState.model = "ollama-qwen3-coder:30b";
|
|
649
|
+
} else {
|
|
650
|
+
trackedState.model = "gpt-5.2";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!trackedState.compactionModel) {
|
|
654
|
+
const m = trackedState.model;
|
|
655
|
+
if (m.startsWith("claude-")) {
|
|
656
|
+
trackedState.compactionModel = "claude-3-5-haiku-latest";
|
|
657
|
+
} else if (m.startsWith("gemini-")) {
|
|
658
|
+
trackedState.compactionModel = "gemini-1.5-flash";
|
|
659
|
+
} else if (m.startsWith("ollama-")) {
|
|
660
|
+
trackedState.compactionModel = "ollama-qwen2.5-coder";
|
|
661
|
+
} else {
|
|
662
|
+
trackedState.compactionModel = "gpt-4o-mini";
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!isOpenAIModel(trackedState.model)) {
|
|
666
|
+
process.env.OPENAI_AGENTS_DISABLE_TRACING = process.env.OPENAI_AGENTS_DISABLE_TRACING || "1";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// lifecycle/readAgentsMD.ts
|
|
671
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
672
|
+
import { join as join4 } from "path";
|
|
673
|
+
function readAgentsMD() {
|
|
674
|
+
const agentsMdExists = existsSync2(join4(trackedState.cwd, "AGENTS.md"));
|
|
675
|
+
if (agentsMdExists) {
|
|
676
|
+
return readFileSync2(join4(trackedState.cwd, "AGENTS.md"), "utf8");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// lifecycle/sayHi.ts
|
|
681
|
+
import chalk4 from "chalk";
|
|
682
|
+
import { existsSync as existsSync3 } from "fs";
|
|
683
|
+
import { join as join5 } from "path";
|
|
684
|
+
function sayHi() {
|
|
685
|
+
const harperAppExists = existsSync3(join5(trackedState.cwd, "config.yaml"));
|
|
686
|
+
console.log(chalk4.dim(`Working directory: ${chalk4.cyan(trackedState.cwd)}`));
|
|
687
|
+
console.log(chalk4.dim(`Harper app detected: ${chalk4.cyan(harperAppExists ? "Yes" : "No")}`));
|
|
688
|
+
console.log(chalk4.dim(`Press Ctrl+C or hit enter twice to exit.
|
|
689
|
+
`));
|
|
690
|
+
harperResponse(
|
|
691
|
+
harperAppExists ? "What do you want to do together today?" : "What kind of Harper app do you want to make together?"
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// tools/files/applyPatchTool.ts
|
|
696
|
+
import { tool } from "@openai/agents";
|
|
697
|
+
import chalk7 from "chalk";
|
|
698
|
+
import { z } from "zod";
|
|
699
|
+
|
|
700
|
+
// utils/files/printDiff.ts
|
|
701
|
+
import chalk5 from "chalk";
|
|
702
|
+
function printDiff(diff) {
|
|
703
|
+
const lines = diff.split("\n");
|
|
704
|
+
for (const line of lines) {
|
|
705
|
+
if (line.startsWith("+")) {
|
|
706
|
+
console.log(chalk5.green(line));
|
|
707
|
+
} else if (line.startsWith("-")) {
|
|
708
|
+
console.log(chalk5.red(line));
|
|
709
|
+
} else {
|
|
710
|
+
console.log(chalk5.dim(line));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// utils/getEnv.ts
|
|
716
|
+
import chalk6 from "chalk";
|
|
717
|
+
var warnedKeys = /* @__PURE__ */ new Set();
|
|
718
|
+
function getEnv(newKey, oldKey) {
|
|
719
|
+
const newValue = process.env[newKey];
|
|
720
|
+
if (newValue !== void 0) {
|
|
721
|
+
return newValue;
|
|
722
|
+
}
|
|
723
|
+
const oldValue = process.env[oldKey];
|
|
724
|
+
if (oldValue !== void 0) {
|
|
725
|
+
if (!warnedKeys.has(oldKey)) {
|
|
726
|
+
console.warn(
|
|
727
|
+
chalk6.yellow(
|
|
728
|
+
`[DEPRECATION NOTICE] The environment variable ${oldKey} is deprecated and will be removed in a future version. Please use ${newKey} instead.`
|
|
729
|
+
)
|
|
730
|
+
);
|
|
731
|
+
warnedKeys.add(oldKey);
|
|
732
|
+
}
|
|
733
|
+
return oldValue;
|
|
734
|
+
}
|
|
735
|
+
return void 0;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// tools/files/workspaceEditor.ts
|
|
739
|
+
import { applyDiff } from "@openai/agents";
|
|
740
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
741
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
742
|
+
import path3 from "path";
|
|
743
|
+
|
|
744
|
+
// utils/files/normalizeDiff.ts
|
|
745
|
+
function normalizeDiff(diff) {
|
|
746
|
+
return diff.split(/\r?\n/).filter((line) => {
|
|
747
|
+
const l = line.trim();
|
|
748
|
+
if (l.startsWith("*** Begin Patch") || l.startsWith("*** End Patch") || l.startsWith("*** Add File:") || l.startsWith("*** Update File:") || l.startsWith("*** Delete File:") || l.startsWith("*** End of File")) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (l.startsWith("update_file:") || l.startsWith("create_file:") || l.startsWith("delete_file:")) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}).join("\n").trim();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// utils/files/paths.ts
|
|
759
|
+
import path2 from "path";
|
|
760
|
+
|
|
761
|
+
// utils/files/aiignore.ts
|
|
762
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
763
|
+
import path from "path";
|
|
764
|
+
var ignorePatterns = [];
|
|
765
|
+
function loadAiIgnore() {
|
|
766
|
+
const ignorePath = path.join(trackedState.cwd, ".aiignore");
|
|
767
|
+
if (existsSync4(ignorePath)) {
|
|
768
|
+
try {
|
|
769
|
+
const content = readFileSync3(ignorePath, "utf8");
|
|
770
|
+
ignorePatterns = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((pattern) => pattern.endsWith("/") || pattern.endsWith("\\") ? pattern.slice(0, -1) : pattern);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
console.error(`Error reading .aiignore: ${error}`);
|
|
773
|
+
ignorePatterns = [];
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
ignorePatterns = [];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function isIgnored(filePath) {
|
|
780
|
+
const directParts = path.normalize(filePath).split(path.sep);
|
|
781
|
+
if (directParts.includes(".aiignore")) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
const absolutePath = path.resolve(trackedState.cwd, filePath);
|
|
785
|
+
const relativePath = path.relative(trackedState.cwd, absolutePath);
|
|
786
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
const normalizedPath = path.normalize(relativePath);
|
|
790
|
+
const parts = normalizedPath.split(path.sep);
|
|
791
|
+
loadAiIgnore();
|
|
792
|
+
if (ignorePatterns.length === 0) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
return ignorePatterns.some((pattern) => {
|
|
796
|
+
if (pattern.startsWith("/")) {
|
|
797
|
+
const normalizedPattern = path.normalize(pattern.substring(1));
|
|
798
|
+
return normalizedPath === normalizedPattern || normalizedPath.startsWith(normalizedPattern + path.sep);
|
|
799
|
+
}
|
|
800
|
+
return parts.some((part, index) => {
|
|
801
|
+
const subPath = parts.slice(index).join(path.sep);
|
|
802
|
+
const normalizedPattern = path.normalize(pattern);
|
|
803
|
+
return subPath === normalizedPattern || subPath.startsWith(normalizedPattern + path.sep);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// utils/files/paths.ts
|
|
809
|
+
function resolvePath(root, relativePath) {
|
|
810
|
+
const resolved = path2.resolve(root, relativePath);
|
|
811
|
+
const relative = path2.relative(root, resolved);
|
|
812
|
+
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
813
|
+
throw new Error(`Operation outside workspace: ${relativePath}`);
|
|
814
|
+
}
|
|
815
|
+
if (isIgnored(resolved)) {
|
|
816
|
+
throw new Error(`Operation restricted by .aiignore: ${relativePath}`);
|
|
817
|
+
}
|
|
818
|
+
return resolved;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// tools/files/workspaceEditor.ts
|
|
822
|
+
var WorkspaceEditor = class {
|
|
823
|
+
root;
|
|
824
|
+
constructor(root) {
|
|
825
|
+
this.root = root;
|
|
826
|
+
}
|
|
827
|
+
async createFile(operation) {
|
|
828
|
+
try {
|
|
829
|
+
const targetPath = resolvePath(this.root(), operation.path);
|
|
830
|
+
await mkdir(path3.dirname(targetPath), { recursive: true });
|
|
831
|
+
const normalizedDiff = normalizeDiff(operation.diff);
|
|
832
|
+
const content = applyDiff("", normalizedDiff, "create");
|
|
833
|
+
await writeFile(targetPath, content, "utf8");
|
|
834
|
+
return { status: "completed", output: `Created ${operation.path}` };
|
|
835
|
+
} catch (err) {
|
|
836
|
+
return { status: "failed", output: `Error creating ${operation.path}: ${String(err)}` };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async updateFile(operation) {
|
|
840
|
+
try {
|
|
841
|
+
const targetPath = resolvePath(this.root(), operation.path);
|
|
842
|
+
if (!existsSync5(targetPath)) {
|
|
843
|
+
return { status: "failed", output: "Error: file not found at path " + targetPath };
|
|
844
|
+
}
|
|
845
|
+
const original = readFileSync4(targetPath, "utf8");
|
|
846
|
+
const normalizedDiff = normalizeDiff(operation.diff);
|
|
847
|
+
const patched = applyDiff(original, normalizedDiff);
|
|
848
|
+
await writeFile(targetPath, patched, "utf8");
|
|
849
|
+
return { status: "completed", output: `Updated ${operation.path}` };
|
|
850
|
+
} catch (err) {
|
|
851
|
+
return { status: "failed", output: `Error updating ${operation.path}: ${String(err)}` };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async deleteFile(operation) {
|
|
855
|
+
try {
|
|
856
|
+
const targetPath = resolvePath(this.root(), operation.path);
|
|
857
|
+
if (!existsSync5(targetPath)) {
|
|
858
|
+
return { status: "failed", output: "Error: file not found at path " + targetPath };
|
|
859
|
+
}
|
|
860
|
+
await rm(targetPath, { force: true });
|
|
861
|
+
return { status: "completed", output: `Deleted ${operation.path}` };
|
|
862
|
+
} catch (err) {
|
|
863
|
+
return { status: "failed", output: `Error deleting ${operation.path}: ${String(err)}` };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// tools/files/applyPatchTool.ts
|
|
869
|
+
var ApplyPatchParameters = z.object({
|
|
870
|
+
type: z.enum(["create_file", "update_file", "delete_file"]).describe("The type of operation to perform."),
|
|
871
|
+
path: z.string().describe("The path to the file to operate on."),
|
|
872
|
+
diff: z.string().optional().default("").describe(
|
|
873
|
+
'The diff to apply. For create_file, every line must start with "+". For update_file, use a headerless unified diff format (start sections with "@@", and use "+", "-", or " " for lines). Do not include markers like "*** Begin Patch" or "*** Add File:".'
|
|
874
|
+
)
|
|
875
|
+
});
|
|
876
|
+
function createApplyPatchTool() {
|
|
877
|
+
const editor = new WorkspaceEditor(() => trackedState.cwd);
|
|
878
|
+
return tool({
|
|
879
|
+
name: "apply_patch",
|
|
880
|
+
description: "Applies a patch (create, update, or delete a file) to the workspace.",
|
|
881
|
+
parameters: ApplyPatchParameters,
|
|
882
|
+
needsApproval: async (runContext, operation, callId) => {
|
|
883
|
+
try {
|
|
884
|
+
if (callId && runContext.isToolApproved({ toolName: "apply_patch", callId })) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
const autoApproved = getEnv("HARPER_AGENT_AUTO_APPROVE_PATCHES", "APPLY_PATCH_AUTO_APPROVE") === "1";
|
|
888
|
+
spinner.stop();
|
|
889
|
+
if (autoApproved) {
|
|
890
|
+
console.log(`
|
|
891
|
+
${chalk7.bold.bgGreen.black(" Apply patch (auto-approved): ")}`);
|
|
892
|
+
} else {
|
|
893
|
+
console.log(`
|
|
894
|
+
${chalk7.bold.bgYellow.black(" Apply patch approval required: ")}`);
|
|
895
|
+
}
|
|
896
|
+
console.log(`${chalk7.bold(operation.type)}: ${operation.path}`);
|
|
897
|
+
if (operation.diff) {
|
|
898
|
+
printDiff(operation.diff);
|
|
899
|
+
}
|
|
900
|
+
if (autoApproved) {
|
|
901
|
+
spinner.start();
|
|
902
|
+
}
|
|
903
|
+
return !autoApproved;
|
|
904
|
+
} catch (err) {
|
|
905
|
+
console.error("apply_patch approval step failed:", err);
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
execute: async (operation) => {
|
|
910
|
+
try {
|
|
911
|
+
switch (operation.type) {
|
|
912
|
+
case "create_file":
|
|
913
|
+
if (!operation.diff) {
|
|
914
|
+
return { status: "failed", output: "Error: diff is required for create_file" };
|
|
915
|
+
}
|
|
916
|
+
return await editor.createFile(operation);
|
|
917
|
+
case "update_file":
|
|
918
|
+
if (!operation.diff) {
|
|
919
|
+
return { status: "failed", output: "Error: diff is required for update_file" };
|
|
920
|
+
}
|
|
921
|
+
return await editor.updateFile(operation);
|
|
922
|
+
case "delete_file":
|
|
923
|
+
return await editor.deleteFile(operation);
|
|
924
|
+
default:
|
|
925
|
+
return { status: "failed", output: `Error: Unknown operation type: ${operation.type}` };
|
|
926
|
+
}
|
|
927
|
+
} catch (err) {
|
|
928
|
+
console.error("hit unexpected error in apply patch tool", err);
|
|
929
|
+
return { status: "failed", output: `apply_patch threw: ${String(err)}` };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// tools/files/changeCwdTool.ts
|
|
936
|
+
import { tool as tool2 } from "@openai/agents";
|
|
937
|
+
import { statSync } from "fs";
|
|
938
|
+
import { z as z2 } from "zod";
|
|
939
|
+
var ToolParameters = z2.object({
|
|
940
|
+
path: z2.string().describe("Directory to switch into. Can be absolute or relative to current workspace.")
|
|
941
|
+
});
|
|
942
|
+
async function execute({ path: path7 }) {
|
|
943
|
+
try {
|
|
944
|
+
const target = resolvePath(trackedState.cwd, path7);
|
|
945
|
+
const stat = statSync(target);
|
|
946
|
+
if (!stat.isDirectory()) {
|
|
947
|
+
console.log(`Path is not a directory: ${target}`);
|
|
948
|
+
return `Path is not a directory: ${target}`;
|
|
949
|
+
}
|
|
950
|
+
process.chdir(target);
|
|
951
|
+
trackedState.cwd = process.cwd();
|
|
952
|
+
console.log(`Switched current working directory to ${trackedState.cwd}`);
|
|
953
|
+
const agentsMDContents = readAgentsMD();
|
|
954
|
+
if (agentsMDContents) {
|
|
955
|
+
if (trackedState.agent) {
|
|
956
|
+
trackedState.agent.instructions = agentsMDContents;
|
|
957
|
+
}
|
|
958
|
+
return `Switched current working directory to ${trackedState.cwd}, with a AGENTS.md file containing:
|
|
959
|
+
${agentsMDContents}`;
|
|
960
|
+
}
|
|
961
|
+
return `Switched current working directory to ${trackedState.cwd}`;
|
|
962
|
+
} catch (err) {
|
|
963
|
+
console.log(`Failed to change directory: ${err.message}`);
|
|
964
|
+
return `Failed to change directory: ${err.message}`;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
var changeCwdTool = tool2({
|
|
968
|
+
name: "changeCwd",
|
|
969
|
+
description: "Changes the current working directory for subsequent tools. Accepts absolute or relative paths.",
|
|
970
|
+
parameters: ToolParameters,
|
|
971
|
+
execute
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// tools/files/egrepTool.ts
|
|
975
|
+
import { tool as tool3 } from "@openai/agents";
|
|
976
|
+
import { execFile } from "child_process";
|
|
977
|
+
import { promisify } from "util";
|
|
978
|
+
import { z as z3 } from "zod";
|
|
979
|
+
var execFileAsync = promisify(execFile);
|
|
980
|
+
var ToolParameters2 = z3.object({
|
|
981
|
+
path: z3.string().describe("The path to start the search from."),
|
|
982
|
+
pattern: z3.string().describe("The pattern to search")
|
|
983
|
+
});
|
|
984
|
+
var egrepTool = tool3({
|
|
985
|
+
name: "egrep",
|
|
986
|
+
description: "File pattern searcher.",
|
|
987
|
+
parameters: ToolParameters2,
|
|
988
|
+
async execute({ path: searchPath, pattern }) {
|
|
989
|
+
try {
|
|
990
|
+
const resolvedPath = resolvePath(trackedState.cwd, searchPath);
|
|
991
|
+
const { stdout } = await execFileAsync("grep", ["-Eir", pattern, resolvedPath]);
|
|
992
|
+
return stdout.split("\n").filter((line) => {
|
|
993
|
+
if (line.trim() === "") {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
const colonIndex = line.indexOf(":");
|
|
997
|
+
if (colonIndex !== -1) {
|
|
998
|
+
const filePath = line.substring(0, colonIndex);
|
|
999
|
+
return !isIgnored(filePath);
|
|
1000
|
+
}
|
|
1001
|
+
return true;
|
|
1002
|
+
}).join("\n");
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
if (error.code === 1) {
|
|
1005
|
+
return "";
|
|
1006
|
+
}
|
|
1007
|
+
return `Error executing egrep command: ${error.stderr || error.message}`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// tools/files/findTool.ts
|
|
1013
|
+
import { tool as tool4 } from "@openai/agents";
|
|
1014
|
+
import { execFile as execFile2 } from "child_process";
|
|
1015
|
+
import { promisify as promisify2 } from "util";
|
|
1016
|
+
import { z as z4 } from "zod";
|
|
1017
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1018
|
+
var ToolParameters3 = z4.object({
|
|
1019
|
+
path: z4.string().describe("The path to start the search from."),
|
|
1020
|
+
iname: z4.string().describe(
|
|
1021
|
+
"Case insensitive, true if the last component of the pathname being examined matches pattern. Special shell pattern matching characters (\u201C[\u201D, \u201C]\u201D, \u201C*\u201D, and \u201C?\u201D) may be used as part of pattern. These characters may be matched explicitly by escaping them with a backslash (\u201C\\\u201D)."
|
|
1022
|
+
)
|
|
1023
|
+
});
|
|
1024
|
+
var findTool = tool4({
|
|
1025
|
+
name: "find",
|
|
1026
|
+
description: "Walk a file hierarchy.",
|
|
1027
|
+
parameters: ToolParameters3,
|
|
1028
|
+
async execute({ path: searchPath, iname }) {
|
|
1029
|
+
try {
|
|
1030
|
+
const resolvedPath = resolvePath(trackedState.cwd, searchPath);
|
|
1031
|
+
const { stdout } = await execFileAsync2("find", [resolvedPath, "-iname", iname]);
|
|
1032
|
+
return stdout.split("\n").filter((line) => line.trim() !== "" && !isIgnored(line)).join("\n");
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
return `Error executing find command: ${error.stderr || error.message}`;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// tools/files/readDirTool.ts
|
|
1040
|
+
import { tool as tool5 } from "@openai/agents";
|
|
1041
|
+
import { readdir } from "fs/promises";
|
|
1042
|
+
import path4 from "path";
|
|
1043
|
+
import { z as z5 } from "zod";
|
|
1044
|
+
var ToolParameters4 = z5.object({
|
|
1045
|
+
directoryName: z5.string().describe("The name of the directory to read.")
|
|
1046
|
+
});
|
|
1047
|
+
var readDirTool = tool5({
|
|
1048
|
+
name: "readDir",
|
|
1049
|
+
description: "Lists the files in a directory.",
|
|
1050
|
+
parameters: ToolParameters4,
|
|
1051
|
+
async execute({ directoryName }) {
|
|
1052
|
+
try {
|
|
1053
|
+
const resolvedPath = resolvePath(trackedState.cwd, directoryName);
|
|
1054
|
+
const files = await readdir(resolvedPath, "utf8");
|
|
1055
|
+
return files.filter((file) => !isIgnored(path4.join(resolvedPath, file)));
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
return `Error reading directory: ${error}`;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// tools/files/readFileTool.ts
|
|
1063
|
+
import { tool as tool6 } from "@openai/agents";
|
|
1064
|
+
import { readFile } from "fs/promises";
|
|
1065
|
+
import { z as z6 } from "zod";
|
|
1066
|
+
var ToolParameters5 = z6.object({
|
|
1067
|
+
fileName: z6.string().describe("The name of the file to read.")
|
|
1068
|
+
});
|
|
1069
|
+
var readFileTool = tool6({
|
|
1070
|
+
name: "readFile",
|
|
1071
|
+
description: "Reads the contents of a specified file.",
|
|
1072
|
+
parameters: ToolParameters5,
|
|
1073
|
+
async execute({ fileName }) {
|
|
1074
|
+
try {
|
|
1075
|
+
const resolvedPath = resolvePath(trackedState.cwd, fileName);
|
|
1076
|
+
return readFile(resolvedPath, "utf8");
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
return `Error reading file: ${error}`;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// tools/general/codeInterpreterTool.ts
|
|
1084
|
+
import { tool as tool7 } from "@openai/agents";
|
|
1085
|
+
import chalk8 from "chalk";
|
|
1086
|
+
import { exec } from "child_process";
|
|
1087
|
+
import { unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1088
|
+
import path5 from "path";
|
|
1089
|
+
import { promisify as promisify3 } from "util";
|
|
1090
|
+
import { z as z7 } from "zod";
|
|
1091
|
+
var execAsync = promisify3(exec);
|
|
1092
|
+
var CodeInterpreterParameters = z7.object({
|
|
1093
|
+
code: z7.string().describe("The code to execute."),
|
|
1094
|
+
language: z7.enum(["python", "javascript"]).optional().default("python").describe(
|
|
1095
|
+
"The programming language of the code."
|
|
1096
|
+
)
|
|
1097
|
+
});
|
|
1098
|
+
var codeInterpreterTool = tool7({
|
|
1099
|
+
name: "code_interpreter",
|
|
1100
|
+
description: "Executes Python or JavaScript code in a local environment. This is useful for data analysis, complex calculations, and more. All code will be executed in the current workspace.",
|
|
1101
|
+
parameters: CodeInterpreterParameters,
|
|
1102
|
+
execute: execute2,
|
|
1103
|
+
needsApproval
|
|
1104
|
+
});
|
|
1105
|
+
async function needsApproval(runContext, { code, language }, callId) {
|
|
1106
|
+
if (callId && runContext.isToolApproved({ toolName: "code_interpreter", callId })) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
const autoApproved = getEnv("HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER", "CODE_INTERPRETER_AUTO_APPROVE") === "1";
|
|
1110
|
+
spinner.stop();
|
|
1111
|
+
if (autoApproved) {
|
|
1112
|
+
console.log(`
|
|
1113
|
+
${chalk8.bold.bgGreen.black(` Code interpreter (${language}, auto-approved): `)}`);
|
|
1114
|
+
} else {
|
|
1115
|
+
console.log(`
|
|
1116
|
+
${chalk8.bold.bgYellow.black(` Code interpreter (${language}) approval required: `)}`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log(chalk8.dim(code));
|
|
1119
|
+
if (autoApproved) {
|
|
1120
|
+
spinner.start();
|
|
1121
|
+
}
|
|
1122
|
+
return !autoApproved;
|
|
1123
|
+
}
|
|
1124
|
+
async function execute2({ code, language }) {
|
|
1125
|
+
const extension = language === "javascript" ? "js" : "py";
|
|
1126
|
+
const interpreter = language === "javascript" ? "node" : "python3";
|
|
1127
|
+
const tempFile = path5.join(trackedState.cwd, `.temp_code_${Date.now()}.${extension}`);
|
|
1128
|
+
try {
|
|
1129
|
+
await writeFile2(tempFile, code, "utf8");
|
|
1130
|
+
const { stdout, stderr } = await execAsync(`${interpreter} ${tempFile}`);
|
|
1131
|
+
return `STDOUT:
|
|
1132
|
+
${stdout}
|
|
1133
|
+
STDERR:
|
|
1134
|
+
${stderr}`;
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
return `Error executing code: ${error.message}
|
|
1137
|
+
STDOUT: ${error.stdout || ""}
|
|
1138
|
+
STDERR: ${error.stderr || ""}`;
|
|
1139
|
+
} finally {
|
|
1140
|
+
await unlink(tempFile).catch(() => {
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// tools/general/setInterpreterAutoApproveTool.ts
|
|
1146
|
+
import { tool as tool8 } from "@openai/agents";
|
|
1147
|
+
import { z as z8 } from "zod";
|
|
1148
|
+
|
|
1149
|
+
// utils/files/updateEnv.ts
|
|
1150
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1151
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
1152
|
+
import { join as join6 } from "path";
|
|
1153
|
+
async function updateEnv(key, value) {
|
|
1154
|
+
process.env[key] = value;
|
|
1155
|
+
const envPath = join6(trackedState.cwd, ".env");
|
|
1156
|
+
let envContent = "";
|
|
1157
|
+
if (existsSync6(envPath)) {
|
|
1158
|
+
envContent = await readFile2(envPath, "utf8");
|
|
1159
|
+
}
|
|
1160
|
+
const regex = new RegExp(`^${key}=.*`, "m");
|
|
1161
|
+
if (regex.test(envContent)) {
|
|
1162
|
+
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
1163
|
+
} else {
|
|
1164
|
+
if (envContent && !envContent.endsWith("\n")) {
|
|
1165
|
+
envContent += "\n";
|
|
1166
|
+
}
|
|
1167
|
+
envContent += `${key}=${value}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
await writeFile3(envPath, envContent);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// tools/general/setInterpreterAutoApproveTool.ts
|
|
1174
|
+
var SetInterpreterAutoApproveParameters = z8.object({
|
|
1175
|
+
autoApprove: z8.boolean()
|
|
1176
|
+
});
|
|
1177
|
+
var setInterpreterAutoApproveTool = tool8({
|
|
1178
|
+
name: "setInterpreterAutoApproveTool",
|
|
1179
|
+
description: "Enable or disable automatic approval for code interpreter by setting HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER=1 or 0 in .env and current process.",
|
|
1180
|
+
parameters: SetInterpreterAutoApproveParameters,
|
|
1181
|
+
needsApproval: async (_runContext, { autoApprove }) => {
|
|
1182
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1183
|
+
return getEnv("HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER", "CODE_INTERPRETER_AUTO_APPROVE") !== newValue;
|
|
1184
|
+
},
|
|
1185
|
+
async execute({ autoApprove }) {
|
|
1186
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1187
|
+
if (getEnv("HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER", "CODE_INTERPRETER_AUTO_APPROVE") === newValue) {
|
|
1188
|
+
return `HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER is already set to ${newValue}.`;
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
await updateEnv("HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER", newValue);
|
|
1192
|
+
return `HARPER_AGENT_AUTO_APPROVE_CODE_INTERPRETER has been set to ${newValue} in .env and current process.`;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
return `Error updating .env file: ${error.message}`;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// tools/general/setPatchAutoApproveTool.ts
|
|
1200
|
+
import { tool as tool9 } from "@openai/agents";
|
|
1201
|
+
import { z as z9 } from "zod";
|
|
1202
|
+
var SetPatchAutoApproveParameters = z9.object({
|
|
1203
|
+
autoApprove: z9.boolean()
|
|
1204
|
+
});
|
|
1205
|
+
var setPatchAutoApproveTool = tool9({
|
|
1206
|
+
name: "setPatchAutoApproveTool",
|
|
1207
|
+
description: "Enable or disable automatic approval for patch commands by setting HARPER_AGENT_AUTO_APPROVE_PATCHES=1 or 0 in .env and current process.",
|
|
1208
|
+
parameters: SetPatchAutoApproveParameters,
|
|
1209
|
+
needsApproval: async (_runContext, { autoApprove }) => {
|
|
1210
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1211
|
+
return getEnv("HARPER_AGENT_AUTO_APPROVE_PATCHES", "APPLY_PATCH_AUTO_APPROVE") !== newValue;
|
|
1212
|
+
},
|
|
1213
|
+
async execute({ autoApprove }) {
|
|
1214
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1215
|
+
if (getEnv("HARPER_AGENT_AUTO_APPROVE_PATCHES", "APPLY_PATCH_AUTO_APPROVE") === newValue) {
|
|
1216
|
+
return `HARPER_AGENT_AUTO_APPROVE_PATCHES is already set to ${newValue}.`;
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
await updateEnv("HARPER_AGENT_AUTO_APPROVE_PATCHES", newValue);
|
|
1220
|
+
return `HARPER_AGENT_AUTO_APPROVE_PATCHES has been set to ${newValue} in .env and current process.`;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
return `Error updating .env file: ${error.message}`;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// tools/general/setShellAutoApproveTool.ts
|
|
1228
|
+
import { tool as tool10 } from "@openai/agents";
|
|
1229
|
+
import { z as z10 } from "zod";
|
|
1230
|
+
var SetShellAutoApproveParameters = z10.object({
|
|
1231
|
+
autoApprove: z10.boolean()
|
|
1232
|
+
});
|
|
1233
|
+
var setShellAutoApproveTool = tool10({
|
|
1234
|
+
name: "setShellAutoApproveTool",
|
|
1235
|
+
description: "Enable or disable automatic approval for shell commands by setting HARPER_AGENT_AUTO_APPROVE_SHELL=1 or 0 in .env and current process.",
|
|
1236
|
+
parameters: SetShellAutoApproveParameters,
|
|
1237
|
+
needsApproval: async (_runContext, { autoApprove }) => {
|
|
1238
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1239
|
+
return getEnv("HARPER_AGENT_AUTO_APPROVE_SHELL", "SHELL_AUTO_APPROVE") !== newValue;
|
|
1240
|
+
},
|
|
1241
|
+
async execute({ autoApprove }) {
|
|
1242
|
+
const newValue = autoApprove ? "1" : "0";
|
|
1243
|
+
if (getEnv("HARPER_AGENT_AUTO_APPROVE_SHELL", "SHELL_AUTO_APPROVE") === newValue) {
|
|
1244
|
+
return `HARPER_AGENT_AUTO_APPROVE_SHELL is already set to ${newValue}.`;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
await updateEnv("HARPER_AGENT_AUTO_APPROVE_SHELL", newValue);
|
|
1248
|
+
return `HARPER_AGENT_AUTO_APPROVE_SHELL has been set to ${newValue} in .env and current process.`;
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
return `Error updating .env file: ${error.message}`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
// tools/general/shellTool.ts
|
|
1256
|
+
import { tool as tool11 } from "@openai/agents";
|
|
1257
|
+
import chalk9 from "chalk";
|
|
1258
|
+
import { z as z11 } from "zod";
|
|
1259
|
+
|
|
1260
|
+
// utils/files/mentionsIgnoredPath.ts
|
|
1261
|
+
function mentionsIgnoredPath(command) {
|
|
1262
|
+
const partRegex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([^\s;&|><]+)/g;
|
|
1263
|
+
let match;
|
|
1264
|
+
while ((match = partRegex.exec(command)) !== null) {
|
|
1265
|
+
const pathCandidate = match[1] ?? match[2] ?? match[3];
|
|
1266
|
+
if (pathCandidate && isIgnored(pathCandidate)) {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// utils/shell/isRiskyCommand.ts
|
|
1274
|
+
var RISKY_COMMANDS = [
|
|
1275
|
+
// File removal
|
|
1276
|
+
/(?:^|[;&|])\s*rm\b/i,
|
|
1277
|
+
/(?:^|[;&|])\s*shred\b/i,
|
|
1278
|
+
/(?:^|[;&|])\s*git\s+clean\b.*\s-f/i,
|
|
1279
|
+
// Database operations
|
|
1280
|
+
/\bdrop\s+(database|table|collection|schema)\b/i,
|
|
1281
|
+
/\btruncate\s+table\b/i,
|
|
1282
|
+
/\bdelete\s+from\b/i,
|
|
1283
|
+
// Disk/System operations
|
|
1284
|
+
/(?:^|[;&|])\s*dd\b.*\bof=/i,
|
|
1285
|
+
/(?:^|[;&|])\s*mkfs\b/i,
|
|
1286
|
+
/(?:^|[;&|])\s*format\b/i,
|
|
1287
|
+
/(?:^|[;&|])\s*fdisk\b/i,
|
|
1288
|
+
/(?:^|[;&|])\s*parted\b/i,
|
|
1289
|
+
// Permissions (broad changes)
|
|
1290
|
+
/(?:^|[;&|])\s*chmod\b.*\s-R/i,
|
|
1291
|
+
/(?:^|[;&|])\s*chown\b.*\s-R/i,
|
|
1292
|
+
// System state
|
|
1293
|
+
/(?:^|[;&|])\s*shutdown\b/i,
|
|
1294
|
+
/(?:^|[;&|])\s*reboot\b/i,
|
|
1295
|
+
/(?:^|[;&|])\s*halt\b/i,
|
|
1296
|
+
/(?:^|[;&|])\s*poweroff\b/i,
|
|
1297
|
+
// Networking/Firewall (potentially destructive to access)
|
|
1298
|
+
/(?:^|[;&|])\s*iptables\b.*\s(-F|-X|-Z)/i,
|
|
1299
|
+
/(?:^|[;&|])\s*ufw\s+reset\b/i,
|
|
1300
|
+
// Dangerous redirections
|
|
1301
|
+
/>\s*\/dev\/(?!null)/i
|
|
1302
|
+
];
|
|
1303
|
+
function isRiskyCommand(command) {
|
|
1304
|
+
const trimmedCommand = command.trim();
|
|
1305
|
+
if (!trimmedCommand) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
return RISKY_COMMANDS.some((pattern) => pattern.test(trimmedCommand));
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// utils/shell/LocalShell.ts
|
|
1312
|
+
import { exec as exec2 } from "child_process";
|
|
1313
|
+
import { promisify as promisify4 } from "util";
|
|
1314
|
+
var execAsync2 = promisify4(exec2);
|
|
1315
|
+
var LocalShell = class {
|
|
1316
|
+
defaultTimeoutMs;
|
|
1317
|
+
constructor(options) {
|
|
1318
|
+
const envValRaw = getEnv("HARPER_AGENT_SHELL_TIMEOUT_MS", "SHELL_DEFAULT_TIMEOUT_MS");
|
|
1319
|
+
const envVal = envValRaw !== void 0 ? Number(envValRaw) : void 0;
|
|
1320
|
+
this.defaultTimeoutMs = options?.defaultTimeoutMs ?? (envVal !== void 0 && !Number.isNaN(envVal) ? envVal : 2e4);
|
|
1321
|
+
}
|
|
1322
|
+
async run(action) {
|
|
1323
|
+
const output = [];
|
|
1324
|
+
for (const command of action.commands) {
|
|
1325
|
+
let stdout = "";
|
|
1326
|
+
let stderr = "";
|
|
1327
|
+
let exitCode = 0;
|
|
1328
|
+
let outcome = {
|
|
1329
|
+
type: "exit",
|
|
1330
|
+
exitCode: 0
|
|
1331
|
+
};
|
|
1332
|
+
try {
|
|
1333
|
+
const { stdout: localStdout, stderr: localStderr } = await execAsync2(
|
|
1334
|
+
command,
|
|
1335
|
+
{
|
|
1336
|
+
cwd: trackedState.cwd,
|
|
1337
|
+
// Prefer per-call timeout, else default
|
|
1338
|
+
timeout: action.timeoutMs ?? this.defaultTimeoutMs,
|
|
1339
|
+
maxBuffer: action.maxOutputLength
|
|
1340
|
+
}
|
|
1341
|
+
);
|
|
1342
|
+
stdout = localStdout;
|
|
1343
|
+
stderr = localStderr;
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
exitCode = typeof error?.code === "number" ? error.code : null;
|
|
1346
|
+
stdout = error?.stdout ?? "";
|
|
1347
|
+
stderr = error?.stderr ?? "";
|
|
1348
|
+
outcome = error?.killed || error?.signal === "SIGTERM" ? { type: "timeout" } : { type: "exit", exitCode };
|
|
1349
|
+
}
|
|
1350
|
+
output.push({
|
|
1351
|
+
command,
|
|
1352
|
+
stdout,
|
|
1353
|
+
stderr,
|
|
1354
|
+
outcome
|
|
1355
|
+
});
|
|
1356
|
+
if (outcome.type === "timeout") {
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
output,
|
|
1362
|
+
providerData: {
|
|
1363
|
+
working_directory: trackedState.cwd
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// tools/general/shellTool.ts
|
|
1370
|
+
var ShellParameters = z11.object({
|
|
1371
|
+
commands: z11.array(z11.string()).describe("The commands to execute.")
|
|
1372
|
+
});
|
|
1373
|
+
var shell = new LocalShell();
|
|
1374
|
+
var shellTool = tool11({
|
|
1375
|
+
name: "shellToolForCommandsWithoutABetterTool",
|
|
1376
|
+
description: "Executes shell commands.",
|
|
1377
|
+
parameters: ShellParameters,
|
|
1378
|
+
execute: async ({ commands }) => {
|
|
1379
|
+
const result = await shell.run({ commands });
|
|
1380
|
+
return result.output.map((o) => {
|
|
1381
|
+
let out = `STDOUT:
|
|
1382
|
+
${o.stdout}
|
|
1383
|
+
STDERR:
|
|
1384
|
+
${o.stderr}`;
|
|
1385
|
+
if (o.outcome.type === "exit") {
|
|
1386
|
+
out += `
|
|
1387
|
+
EXIT CODE: ${o.outcome.exitCode}`;
|
|
1388
|
+
} else {
|
|
1389
|
+
out += `
|
|
1390
|
+
TIMEOUT`;
|
|
1391
|
+
}
|
|
1392
|
+
return out;
|
|
1393
|
+
}).join("\n---\n");
|
|
1394
|
+
},
|
|
1395
|
+
needsApproval: async (runContext, { commands }, callId) => {
|
|
1396
|
+
if (callId && runContext.isToolApproved({ toolName: "shell", callId })) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
const foundRiskyCommand = commands.find((command) => isRiskyCommand(command));
|
|
1400
|
+
const foundIgnoredInteraction = commands.find((command) => mentionsIgnoredPath(command));
|
|
1401
|
+
const autoApproved = getEnv("HARPER_AGENT_AUTO_APPROVE_SHELL", "SHELL_AUTO_APPROVE") === "1" && !foundRiskyCommand && !foundIgnoredInteraction;
|
|
1402
|
+
spinner.stop();
|
|
1403
|
+
if (autoApproved) {
|
|
1404
|
+
console.log(
|
|
1405
|
+
chalk9.bold.bgGreen.black("\n Shell command (auto-approved): \n")
|
|
1406
|
+
);
|
|
1407
|
+
} else if (foundRiskyCommand) {
|
|
1408
|
+
console.log(
|
|
1409
|
+
chalk9.bold.bgYellow.black("\n Shell command approval of risky command required: \n")
|
|
1410
|
+
);
|
|
1411
|
+
} else if (foundIgnoredInteraction) {
|
|
1412
|
+
console.log(
|
|
1413
|
+
chalk9.bold.bgYellow.black("\n Shell command approval of ignored file interaction required: \n")
|
|
1414
|
+
);
|
|
1415
|
+
} else {
|
|
1416
|
+
console.log(
|
|
1417
|
+
chalk9.bold.bgYellow.black("\n Shell command approval required: \n")
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
for (const cmd of commands) {
|
|
1421
|
+
console.log(chalk9.dim(` > ${cmd}`));
|
|
1422
|
+
}
|
|
1423
|
+
if (autoApproved) {
|
|
1424
|
+
spinner.start();
|
|
1425
|
+
}
|
|
1426
|
+
return !autoApproved;
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// tools/git/gitAddTool.ts
|
|
1431
|
+
import { tool as tool12 } from "@openai/agents";
|
|
1432
|
+
import { execFile as execFile3 } from "child_process";
|
|
1433
|
+
import { promisify as promisify5 } from "util";
|
|
1434
|
+
import { z as z12 } from "zod";
|
|
1435
|
+
var execFileAsync3 = promisify5(execFile3);
|
|
1436
|
+
var GitAddParameters = z12.object({
|
|
1437
|
+
files: z12.array(z12.string()).describe("The files to add. If not provided, all changes will be added.")
|
|
1438
|
+
});
|
|
1439
|
+
var gitAddTool = tool12({
|
|
1440
|
+
name: "gitAddTool",
|
|
1441
|
+
description: "Add file contents to the index.",
|
|
1442
|
+
parameters: GitAddParameters,
|
|
1443
|
+
async execute({ files }) {
|
|
1444
|
+
try {
|
|
1445
|
+
const args = ["add"];
|
|
1446
|
+
if (!files || files.length === 0) {
|
|
1447
|
+
args.push(".");
|
|
1448
|
+
} else {
|
|
1449
|
+
args.push(...files);
|
|
1450
|
+
}
|
|
1451
|
+
const { stdout, stderr } = await execFileAsync3("git", args);
|
|
1452
|
+
return `Success: ${stdout || stderr || "Files added to index"}`;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// tools/git/gitBranchTool.ts
|
|
1460
|
+
import { tool as tool13 } from "@openai/agents";
|
|
1461
|
+
import { execFile as execFile4 } from "child_process";
|
|
1462
|
+
import { promisify as promisify6 } from "util";
|
|
1463
|
+
import { z as z13 } from "zod";
|
|
1464
|
+
var execFileAsync4 = promisify6(execFile4);
|
|
1465
|
+
var GitBranchParameters = z13.object({
|
|
1466
|
+
branchName: z13.string().describe("The name of the branch to create or switch to."),
|
|
1467
|
+
create: z13.boolean().optional().default(false).describe("Whether to create a new branch.")
|
|
1468
|
+
});
|
|
1469
|
+
var gitBranchTool = tool13({
|
|
1470
|
+
name: "gitBranchTool",
|
|
1471
|
+
description: "Create or switch to a git branch.",
|
|
1472
|
+
parameters: GitBranchParameters,
|
|
1473
|
+
needsApproval: true,
|
|
1474
|
+
async execute({ branchName, create }) {
|
|
1475
|
+
try {
|
|
1476
|
+
const args = create ? ["checkout", "-b", branchName] : ["checkout", branchName];
|
|
1477
|
+
const { stdout, stderr } = await execFileAsync4("git", args);
|
|
1478
|
+
return `Success: ${stdout || stderr || `Switched to branch ${branchName}`}`;
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// tools/git/gitCommitTool.ts
|
|
1486
|
+
import { tool as tool14 } from "@openai/agents";
|
|
1487
|
+
import { execFile as execFile5 } from "child_process";
|
|
1488
|
+
import { promisify as promisify7 } from "util";
|
|
1489
|
+
import { z as z14 } from "zod";
|
|
1490
|
+
var execFileAsync5 = promisify7(execFile5);
|
|
1491
|
+
var GitCommitParameters = z14.object({
|
|
1492
|
+
message: z14.string().describe("The commit message."),
|
|
1493
|
+
addAll: z14.boolean().optional().default(false).describe(
|
|
1494
|
+
"Whether to add all changes before committing (git commit -am)."
|
|
1495
|
+
)
|
|
1496
|
+
});
|
|
1497
|
+
var gitCommitTool = tool14({
|
|
1498
|
+
name: "gitCommitTool",
|
|
1499
|
+
description: "Commit changes to the repository.",
|
|
1500
|
+
parameters: GitCommitParameters,
|
|
1501
|
+
async execute({ message, addAll }) {
|
|
1502
|
+
try {
|
|
1503
|
+
const args = addAll ? ["commit", "-am", message] : ["commit", "-m", message];
|
|
1504
|
+
const { stdout, stderr } = await execFileAsync5("git", args);
|
|
1505
|
+
return `Success: ${stdout || stderr || "Changes committed"}`;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// tools/git/gitLogTool.ts
|
|
1513
|
+
import { tool as tool15 } from "@openai/agents";
|
|
1514
|
+
import { execFile as execFile6 } from "child_process";
|
|
1515
|
+
import { promisify as promisify8 } from "util";
|
|
1516
|
+
import { z as z15 } from "zod";
|
|
1517
|
+
var execFileAsync6 = promisify8(execFile6);
|
|
1518
|
+
var GitLogParameters = z15.object({
|
|
1519
|
+
count: z15.number().optional().default(10).describe("Number of commits to show."),
|
|
1520
|
+
oneline: z15.boolean().optional().default(true).describe("Whether to show log in oneline format.")
|
|
1521
|
+
});
|
|
1522
|
+
var gitLogTool = tool15({
|
|
1523
|
+
name: "gitLogTool",
|
|
1524
|
+
description: "Show commit logs.",
|
|
1525
|
+
parameters: GitLogParameters,
|
|
1526
|
+
async execute({ count, oneline }) {
|
|
1527
|
+
try {
|
|
1528
|
+
const args = ["log", "-n", count.toString()];
|
|
1529
|
+
if (oneline) {
|
|
1530
|
+
args.push("--oneline");
|
|
1531
|
+
}
|
|
1532
|
+
const { stdout, stderr } = await execFileAsync6("git", args);
|
|
1533
|
+
return stdout || stderr || "No log output";
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
// tools/git/gitStashTool.ts
|
|
1541
|
+
import { tool as tool16 } from "@openai/agents";
|
|
1542
|
+
import { execFile as execFile7 } from "child_process";
|
|
1543
|
+
import { promisify as promisify9 } from "util";
|
|
1544
|
+
import { z as z16 } from "zod";
|
|
1545
|
+
var execFileAsync7 = promisify9(execFile7);
|
|
1546
|
+
var allowedActions = ["push", "pop", "apply", "list"];
|
|
1547
|
+
var GitStashParameters = z16.object({
|
|
1548
|
+
action: z16.string().describe("The stash action to perform: " + allowedActions.join(", ")),
|
|
1549
|
+
message: z16.string().describe("A message for the stash change.")
|
|
1550
|
+
});
|
|
1551
|
+
var gitStashTool = tool16({
|
|
1552
|
+
name: "gitStashTool",
|
|
1553
|
+
description: "Stash changes or apply a stash.",
|
|
1554
|
+
parameters: GitStashParameters,
|
|
1555
|
+
async execute({ action, message }) {
|
|
1556
|
+
try {
|
|
1557
|
+
if (!allowedActions.includes(action)) {
|
|
1558
|
+
return `Error: Invalid action '${action}'. Allowed actions are: ${allowedActions.join(", ")}`;
|
|
1559
|
+
}
|
|
1560
|
+
const args = ["stash", action];
|
|
1561
|
+
if (action === "push" && message) {
|
|
1562
|
+
args.push("-m", message);
|
|
1563
|
+
}
|
|
1564
|
+
const { stdout, stderr } = await execFileAsync7("git", args);
|
|
1565
|
+
return `Success: ${stdout || stderr || `Git stash ${action} completed`}`;
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// tools/git/gitStatusTool.ts
|
|
1573
|
+
import { tool as tool17 } from "@openai/agents";
|
|
1574
|
+
import { execFile as execFile8 } from "child_process";
|
|
1575
|
+
import { promisify as promisify10 } from "util";
|
|
1576
|
+
import { z as z17 } from "zod";
|
|
1577
|
+
var execFileAsync8 = promisify10(execFile8);
|
|
1578
|
+
var GitStatusParameters = z17.object({
|
|
1579
|
+
short: z17.boolean().optional().default(false).describe("Whether to show the status in short format.")
|
|
1580
|
+
});
|
|
1581
|
+
var gitStatusTool = tool17({
|
|
1582
|
+
name: "gitStatusTool",
|
|
1583
|
+
description: "Show the working tree status.",
|
|
1584
|
+
parameters: GitStatusParameters,
|
|
1585
|
+
async execute({ short }) {
|
|
1586
|
+
try {
|
|
1587
|
+
const args = ["status"];
|
|
1588
|
+
if (short) {
|
|
1589
|
+
args.push("--short");
|
|
1590
|
+
}
|
|
1591
|
+
const { stdout, stderr } = await execFileAsync8("git", args);
|
|
1592
|
+
return stdout || stderr || "No status output";
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
// tools/git/gitWorkspaceTool.ts
|
|
1600
|
+
import { tool as tool18 } from "@openai/agents";
|
|
1601
|
+
import { execFile as execFile9 } from "child_process";
|
|
1602
|
+
import { promisify as promisify11 } from "util";
|
|
1603
|
+
import { z as z18 } from "zod";
|
|
1604
|
+
var execFileAsync9 = promisify11(execFile9);
|
|
1605
|
+
var GitWorkspaceParameters = z18.object({
|
|
1606
|
+
path: z18.string().describe("The path where the new workspace (worktree) should be created."),
|
|
1607
|
+
branchName: z18.string().describe("The name of the branch to use in the new workspace."),
|
|
1608
|
+
createBranch: z18.boolean().optional().default(false).describe("Whether to create a new branch for this workspace.")
|
|
1609
|
+
});
|
|
1610
|
+
var gitWorkspaceTool = tool18({
|
|
1611
|
+
name: "gitWorkspaceTool",
|
|
1612
|
+
description: "Create a new workspace (git worktree) for parallel work.",
|
|
1613
|
+
parameters: GitWorkspaceParameters,
|
|
1614
|
+
async execute({ path: workspacePath, branchName, createBranch }) {
|
|
1615
|
+
try {
|
|
1616
|
+
const resolvedPath = resolvePath(trackedState.cwd, workspacePath);
|
|
1617
|
+
const args = createBranch ? ["worktree", "add", "-b", branchName, resolvedPath] : ["worktree", "add", resolvedPath, branchName];
|
|
1618
|
+
const { stdout, stderr } = await execFileAsync9("git", args);
|
|
1619
|
+
return `Success: ${stdout || stderr || `Created workspace at ${resolvedPath}`}`;
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return `Error: ${error.stderr || error.message}`;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// tools/harper/checkHarperStatusTool.ts
|
|
1627
|
+
import { tool as tool19 } from "@openai/agents";
|
|
1628
|
+
import { z as z19 } from "zod";
|
|
1629
|
+
var ToolParameters6 = z19.object({});
|
|
1630
|
+
var checkHarperStatusTool = tool19({
|
|
1631
|
+
name: "checkHarperStatusTool",
|
|
1632
|
+
description: "Checks if a Harper application is currently running.",
|
|
1633
|
+
parameters: ToolParameters6,
|
|
1634
|
+
async execute() {
|
|
1635
|
+
if (harperProcess.running) {
|
|
1636
|
+
return "A Harper application is currently running.";
|
|
1637
|
+
} else {
|
|
1638
|
+
return "No Harper application is currently running.";
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// tools/harper/createNewHarperApplicationTool.ts
|
|
1644
|
+
import { tool as tool20 } from "@openai/agents";
|
|
1645
|
+
import { execSync as execSync3 } from "child_process";
|
|
1646
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1647
|
+
import path6 from "path";
|
|
1648
|
+
import { z as z20 } from "zod";
|
|
1649
|
+
|
|
1650
|
+
// utils/package/buildHarperCreateCommand.ts
|
|
1651
|
+
function buildCreateCommand(pm, appName, template) {
|
|
1652
|
+
switch (pm) {
|
|
1653
|
+
case "deno":
|
|
1654
|
+
return { cmd: `deno init --npm harper "${appName}" 2>&1`, label: "deno init --npm harper" };
|
|
1655
|
+
case "npm":
|
|
1656
|
+
return {
|
|
1657
|
+
cmd: `npm create harper@latest --yes "${appName}" -- --no-interactive --template ${template} 2>&1`,
|
|
1658
|
+
label: "npm create harper@latest"
|
|
1659
|
+
};
|
|
1660
|
+
default:
|
|
1661
|
+
return {
|
|
1662
|
+
cmd: `${pm} create harper "${appName}" --no-interactive --template ${template} 2>&1`,
|
|
1663
|
+
label: `${pm} create harper`
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// utils/package/isPackageManagerAvailable.ts
|
|
1669
|
+
import { execSync as execSync2 } from "child_process";
|
|
1670
|
+
function isPackageManagerAvailable(cmd) {
|
|
1671
|
+
try {
|
|
1672
|
+
execSync2(`${cmd} --version`, { stdio: "ignore" });
|
|
1673
|
+
return true;
|
|
1674
|
+
} catch {
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// utils/package/pickPreferredPackageManager.ts
|
|
1680
|
+
function pickPreferredPackageManager() {
|
|
1681
|
+
const preferred = ["yarn", "pnpm", "bun", "deno"];
|
|
1682
|
+
for (const pm of preferred) {
|
|
1683
|
+
if (isPackageManagerAvailable(pm)) {
|
|
1684
|
+
return pm;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return "npm";
|
|
1688
|
+
}
|
|
1689
|
+
var PM_DISPLAY = {
|
|
1690
|
+
yarn: "Yarn",
|
|
1691
|
+
pnpm: "PNPM",
|
|
1692
|
+
bun: "Bun",
|
|
1693
|
+
deno: "Deno",
|
|
1694
|
+
npm: "NPM"
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
// tools/harper/createNewHarperApplicationTool.ts
|
|
1698
|
+
var ToolParameters7 = z20.object({
|
|
1699
|
+
directoryName: z20.string().describe("The name of the directory to create the application in."),
|
|
1700
|
+
template: z20.enum(["vanilla-ts", "vanilla", "react-ts", "react"]).optional().describe("The template to use for the new application. Defaults to vanilla-ts.").default("vanilla-ts")
|
|
1701
|
+
});
|
|
1702
|
+
async function execute3({ directoryName, template }) {
|
|
1703
|
+
const currentCwd = trackedState.cwd;
|
|
1704
|
+
const resolvedPath = resolvePath(currentCwd, directoryName);
|
|
1705
|
+
const isCurrentDir = resolvedPath === currentCwd;
|
|
1706
|
+
const executionCwd = isCurrentDir ? resolvedPath : path6.dirname(resolvedPath);
|
|
1707
|
+
const appName = isCurrentDir ? "." : path6.basename(resolvedPath);
|
|
1708
|
+
try {
|
|
1709
|
+
console.log(`Creating new Harper application in ${resolvedPath} using template ${template}...`);
|
|
1710
|
+
const pm = pickPreferredPackageManager();
|
|
1711
|
+
const { cmd, label } = buildCreateCommand(pm, appName, template);
|
|
1712
|
+
console.log(`Detected ${PM_DISPLAY[pm]}. Executing: ${label} in ${executionCwd} for ${appName}`);
|
|
1713
|
+
execSync3(cmd, {
|
|
1714
|
+
cwd: executionCwd,
|
|
1715
|
+
encoding: "utf8"
|
|
1716
|
+
});
|
|
1717
|
+
console.log(`Initializing new Git repository in ${resolvedPath}...`);
|
|
1718
|
+
execSync3("git init", { cwd: resolvedPath, stdio: "ignore" });
|
|
1719
|
+
const switchedDir = await execute({ path: resolvedPath });
|
|
1720
|
+
const agentsMdExists = existsSync7(path6.join(resolvedPath, "AGENTS.md"));
|
|
1721
|
+
let returnMsg = `Successfully created a new Harper application in '${resolvedPath}' using template '${template}' with a matching Git repository initialized. ${switchedDir}.`;
|
|
1722
|
+
if (agentsMdExists) {
|
|
1723
|
+
returnMsg += ` I found an AGENTS.md file in the new application \u2013 I strongly suggest you read it next to understand how to use your new skills!`;
|
|
1724
|
+
}
|
|
1725
|
+
returnMsg += ` Use the readDir and readFile tools to inspect the contents of the application.`;
|
|
1726
|
+
return returnMsg;
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
let errorMsg = `Error creating new Harper application: ${error.message}`;
|
|
1729
|
+
if (error.stdout) {
|
|
1730
|
+
errorMsg += `
|
|
1731
|
+
|
|
1732
|
+
Command Output:
|
|
1733
|
+
${error.stdout}`;
|
|
1734
|
+
}
|
|
1735
|
+
return errorMsg;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
var createNewHarperApplicationTool = tool20({
|
|
1739
|
+
name: "createNewHarperApplicationTool",
|
|
1740
|
+
description: "Creates a new Harper application using the best available package manager (yarn/pnpm/bun/deno, falling back to npm).",
|
|
1741
|
+
parameters: ToolParameters7,
|
|
1742
|
+
execute: execute3
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// tools/harper/getHarperConfigSchemaTool.ts
|
|
1746
|
+
import { tool as tool21 } from "@openai/agents";
|
|
1747
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1748
|
+
import { createRequire } from "module";
|
|
1749
|
+
import { dirname, join as join7 } from "path";
|
|
1750
|
+
import { z as z21 } from "zod";
|
|
1751
|
+
var ToolParameters8 = z21.object({
|
|
1752
|
+
schemaType: z21.enum(["app", "root"]).describe(
|
|
1753
|
+
'The type of configuration schema to retrieve: "app" for application configuration or "root" for root Harper configuration.'
|
|
1754
|
+
)
|
|
1755
|
+
});
|
|
1756
|
+
var getHarperConfigSchemaTool = tool21({
|
|
1757
|
+
name: "getHarperConfigSchemaTool",
|
|
1758
|
+
description: "Returns the JSON schema for HarperDB configuration files (either app or root), which describes the config.yaml or harperdb-config.yaml files.",
|
|
1759
|
+
parameters: ToolParameters8,
|
|
1760
|
+
async execute({ schemaType }) {
|
|
1761
|
+
try {
|
|
1762
|
+
return await readFile3(
|
|
1763
|
+
join7(
|
|
1764
|
+
dirname(createRequire(import.meta.url).resolve("harperdb")),
|
|
1765
|
+
`config-${schemaType}.schema.json`
|
|
1766
|
+
),
|
|
1767
|
+
"utf8"
|
|
1768
|
+
);
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
return `Error reading HarperDB ${schemaType} configuration schema: ${error}`;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// tools/harper/getHarperResourceInterfaceTool.ts
|
|
1776
|
+
import { tool as tool22 } from "@openai/agents";
|
|
1777
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1778
|
+
import { createRequire as createRequire2 } from "module";
|
|
1779
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
1780
|
+
import { z as z22 } from "zod";
|
|
1781
|
+
var ToolParameters9 = z22.object({
|
|
1782
|
+
resourceFile: z22.enum([
|
|
1783
|
+
"ResourceInterfaceV2",
|
|
1784
|
+
"ResourceInterface",
|
|
1785
|
+
"Table",
|
|
1786
|
+
"Resource",
|
|
1787
|
+
"ResourceV2"
|
|
1788
|
+
]).describe(
|
|
1789
|
+
"The resource-related definition file to read. Defaults to ResourceInterfaceV2."
|
|
1790
|
+
).default("ResourceInterfaceV2")
|
|
1791
|
+
});
|
|
1792
|
+
var getHarperResourceInterfaceTool = tool22({
|
|
1793
|
+
name: "getHarperResourceInterfaceTool",
|
|
1794
|
+
description: "Reads HarperDB resource interface and class definitions (like ResourceInterfaceV2.d.ts) to understand how resources and tables are structured.",
|
|
1795
|
+
parameters: ToolParameters9,
|
|
1796
|
+
async execute({ resourceFile }) {
|
|
1797
|
+
try {
|
|
1798
|
+
return await readFile4(
|
|
1799
|
+
join8(
|
|
1800
|
+
dirname2(createRequire2(import.meta.url).resolve("harperdb")),
|
|
1801
|
+
"resources",
|
|
1802
|
+
`${resourceFile}.d.ts`
|
|
1803
|
+
),
|
|
1804
|
+
"utf8"
|
|
1805
|
+
);
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
return `Error reading HarperDB resource file ${resourceFile}: ${error}`;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// tools/harper/getHarperSchemaGraphQLTool.ts
|
|
1813
|
+
import { tool as tool23 } from "@openai/agents";
|
|
1814
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1815
|
+
import { createRequire as createRequire3 } from "module";
|
|
1816
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
1817
|
+
import { z as z23 } from "zod";
|
|
1818
|
+
var ToolParameters10 = z23.object({});
|
|
1819
|
+
var getHarperSchemaGraphQLTool = tool23({
|
|
1820
|
+
name: "getHarperSchemaGraphQLTool",
|
|
1821
|
+
description: "Returns the GraphQL schema for HarperDB schema files, which define the structure of HarperDB database tables.",
|
|
1822
|
+
parameters: ToolParameters10,
|
|
1823
|
+
async execute() {
|
|
1824
|
+
try {
|
|
1825
|
+
return await readFile5(
|
|
1826
|
+
join9(
|
|
1827
|
+
dirname3(createRequire3(import.meta.url).resolve("harperdb")),
|
|
1828
|
+
`schema.graphql`
|
|
1829
|
+
),
|
|
1830
|
+
"utf8"
|
|
1831
|
+
);
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
return `Error reading HarperDB GraphQL schema: ${error}`;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// tools/harper/getHarperSkillTool.ts
|
|
1839
|
+
import { tool as tool24 } from "@openai/agents";
|
|
1840
|
+
import { readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
1841
|
+
import { createRequire as createRequire4 } from "module";
|
|
1842
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
1843
|
+
import { z as z24 } from "zod";
|
|
1844
|
+
var createHarper = dirname4(createRequire4(import.meta.url).resolve("create-harper"));
|
|
1845
|
+
var agentsMarkdown = join10(
|
|
1846
|
+
createHarper,
|
|
1847
|
+
"AGENTS.md"
|
|
1848
|
+
);
|
|
1849
|
+
var skillsDir = join10(
|
|
1850
|
+
createHarper,
|
|
1851
|
+
"template-vanilla",
|
|
1852
|
+
"skills"
|
|
1853
|
+
);
|
|
1854
|
+
var skillLinkRegex = /\[[^\]]+]\(skills\/([^)]+)\.md\)/g;
|
|
1855
|
+
var skills = getSkills();
|
|
1856
|
+
var ToolParameters11 = z24.object({
|
|
1857
|
+
skill: z24.enum(skills.length > 0 ? skills : ["none"]).describe(
|
|
1858
|
+
"The name of the skill to retrieve."
|
|
1859
|
+
)
|
|
1860
|
+
});
|
|
1861
|
+
var getHarperSkillTool = tool24({
|
|
1862
|
+
name: "getHarperSkill",
|
|
1863
|
+
description: getSkillsDescription(),
|
|
1864
|
+
parameters: ToolParameters11,
|
|
1865
|
+
execute: execute4
|
|
1866
|
+
});
|
|
1867
|
+
function getSkillsDescription() {
|
|
1868
|
+
try {
|
|
1869
|
+
return readFileSync5(agentsMarkdown, "utf8").replace("This repository contains", "This tool describes").replace(skillLinkRegex, "$1");
|
|
1870
|
+
} catch {
|
|
1871
|
+
return "Returns the contents of a Harper skill markdown file. Skills provide guidance on developing Harper applications.";
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function getSkills() {
|
|
1875
|
+
try {
|
|
1876
|
+
return readdirSync(skillsDir).filter((file) => file.endsWith(".md")).map((file) => file.replace(".md", ""));
|
|
1877
|
+
} catch {
|
|
1878
|
+
return [];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async function execute4({ skill }) {
|
|
1882
|
+
if (skill === "none") {
|
|
1883
|
+
return "No skills found.";
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
const filePath = join10(skillsDir, `${skill}.md`);
|
|
1887
|
+
return readFileSync5(filePath, "utf8");
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
return `Error reading Harper skill "${skill}": ${error}`;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// tools/harper/hitHarperAPITool.ts
|
|
1894
|
+
import { tool as tool25 } from "@openai/agents";
|
|
1895
|
+
import { z as z25 } from "zod";
|
|
1896
|
+
var ToolParameters12 = z25.object({
|
|
1897
|
+
method: z25.enum(["POST", "GET", "PUT", "DELETE"]).optional().default("GET").describe(
|
|
1898
|
+
'The HTTP method to use, defaults to "get". Notably, POST and PUT require the full body be sent.'
|
|
1899
|
+
),
|
|
1900
|
+
path: z25.string().optional().default("/openapi").describe("The path to fetch from localhost (defaults to /openapi)."),
|
|
1901
|
+
port: z25.number().optional().default(harperProcess.httpPort).describe(
|
|
1902
|
+
"The port to fetch from localhost (defaults to the running Harper port)."
|
|
1903
|
+
),
|
|
1904
|
+
body: z25.string().optional().default("").describe("An optional JSON string body to send along with the request.")
|
|
1905
|
+
});
|
|
1906
|
+
var hitHarperAPITool = tool25({
|
|
1907
|
+
name: "hitHarperAPITool",
|
|
1908
|
+
description: "Performs a request against the running Harper API. Use /openapi to look up Harper APIs.",
|
|
1909
|
+
parameters: ToolParameters12,
|
|
1910
|
+
needsApproval: async (runContext, input, callId) => {
|
|
1911
|
+
if (callId && runContext.isToolApproved({ toolName: "hitHarperAPITool", callId })) {
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
if (input.method === "DELETE") {
|
|
1915
|
+
const segments = (input.path || "").split("/").filter(Boolean);
|
|
1916
|
+
if (segments.length <= 1) {
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return false;
|
|
1921
|
+
},
|
|
1922
|
+
async execute({ path: path7 = "/openapi", port, method = "GET", body }) {
|
|
1923
|
+
try {
|
|
1924
|
+
const effectivePort = port ?? (harperProcess.running ? harperProcess.httpPort : void 0);
|
|
1925
|
+
if (!effectivePort) {
|
|
1926
|
+
return `Error: No Harper application is currently running and no port was specified.`;
|
|
1927
|
+
}
|
|
1928
|
+
const response = await fetch(
|
|
1929
|
+
`http://localhost:${effectivePort}${path7.startsWith("/") ? "" : "/"}${path7}`,
|
|
1930
|
+
{
|
|
1931
|
+
method,
|
|
1932
|
+
headers: body ? { "Content-Type": "application/json" } : {},
|
|
1933
|
+
body: body || null
|
|
1934
|
+
}
|
|
1935
|
+
);
|
|
1936
|
+
if (!response.ok) {
|
|
1937
|
+
return `Error: Received non-ok response from fetch: ${response.statusText} (${response.status})`;
|
|
1938
|
+
}
|
|
1939
|
+
return await response.text();
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
return `Error: fetch failed: ${error}`;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// tools/harper/openHarperInBrowserTool.ts
|
|
1947
|
+
import { tool as tool26 } from "@openai/agents";
|
|
1948
|
+
import spawn2 from "cross-spawn";
|
|
1949
|
+
import { platform } from "os";
|
|
1950
|
+
import { z as z26 } from "zod";
|
|
1951
|
+
var alreadyOpened = false;
|
|
1952
|
+
var ToolParameters13 = z26.object({});
|
|
1953
|
+
var openHarperInBrowserTool = tool26({
|
|
1954
|
+
name: "openHarperInBrowserTool",
|
|
1955
|
+
description: "Opens the running Harper app in the user's browser.",
|
|
1956
|
+
parameters: ToolParameters13,
|
|
1957
|
+
async execute() {
|
|
1958
|
+
try {
|
|
1959
|
+
if (alreadyOpened) {
|
|
1960
|
+
return `Browser is already open.`;
|
|
1961
|
+
}
|
|
1962
|
+
if (!harperProcess.running) {
|
|
1963
|
+
return `Error: No Harper application is currently running.`;
|
|
1964
|
+
}
|
|
1965
|
+
const url = `http://localhost:${harperProcess.httpPort}/`;
|
|
1966
|
+
const p = platform();
|
|
1967
|
+
if (p === "darwin") {
|
|
1968
|
+
spawn2("open", [url]);
|
|
1969
|
+
} else if (p === "win32") {
|
|
1970
|
+
spawn2("start", ["", url]);
|
|
1971
|
+
} else {
|
|
1972
|
+
spawn2("xdg-open", [url]);
|
|
1973
|
+
}
|
|
1974
|
+
alreadyOpened = true;
|
|
1975
|
+
return `Successfully opened '${url}' in the browser.`;
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
return `Error opening browser: ${error}`;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// tools/harper/readHarperLogsTool.ts
|
|
1983
|
+
import { tool as tool27 } from "@openai/agents";
|
|
1984
|
+
import { z as z27 } from "zod";
|
|
1985
|
+
var ToolParameters14 = z27.object({});
|
|
1986
|
+
var readHarperLogsTool = tool27({
|
|
1987
|
+
name: "readHarperLogsTool",
|
|
1988
|
+
description: "Reads the most recent console logs of a started Harper app and clears them so that subsequent reads will only show new logs.",
|
|
1989
|
+
parameters: ToolParameters14,
|
|
1990
|
+
async execute() {
|
|
1991
|
+
if (!harperProcess.running) {
|
|
1992
|
+
return `Error: No Harper application is currently running.`;
|
|
1993
|
+
}
|
|
1994
|
+
try {
|
|
1995
|
+
const logs = harperProcess.getAndClearLogs();
|
|
1996
|
+
return logs || "No logs available yet.";
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
return `Error reading Harper application logs: ${error}`;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
// tools/harper/startHarperTool.ts
|
|
2004
|
+
import { tool as tool28 } from "@openai/agents";
|
|
2005
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2006
|
+
import { basename, resolve } from "path";
|
|
2007
|
+
import { z as z28 } from "zod";
|
|
2008
|
+
|
|
2009
|
+
// utils/promises/sleep.ts
|
|
2010
|
+
function sleep(ms) {
|
|
2011
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// tools/harper/startHarperTool.ts
|
|
2015
|
+
var ToolParameters15 = z28.object({
|
|
2016
|
+
directoryName: z28.string().describe("The name of the directory that the Harper app is in.")
|
|
2017
|
+
});
|
|
2018
|
+
var startHarperTool = tool28({
|
|
2019
|
+
name: "startHarperTool",
|
|
2020
|
+
description: "Starts a Harper app background process, allowing you to observe the app in action (by readHarperLogsTool, hitHarperAPITool, openHarperInBrowserTool, etc).",
|
|
2021
|
+
parameters: ToolParameters15,
|
|
2022
|
+
async execute({ directoryName }) {
|
|
2023
|
+
if (isIgnored(directoryName)) {
|
|
2024
|
+
return `Error: Target directory ${directoryName} is restricted by .aiignore`;
|
|
2025
|
+
}
|
|
2026
|
+
if (harperProcess.running) {
|
|
2027
|
+
return `Success! A Harper application is already running, and will auto-reload as changes are made.`;
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
let effectiveDirectory = directoryName;
|
|
2031
|
+
const candidatePath = resolve(process.cwd(), directoryName);
|
|
2032
|
+
if (!existsSync8(candidatePath)) {
|
|
2033
|
+
const cwd = process.cwd();
|
|
2034
|
+
if (basename(cwd) === directoryName) {
|
|
2035
|
+
effectiveDirectory = cwd;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
harperProcess.start(effectiveDirectory);
|
|
2039
|
+
await sleep(5e3);
|
|
2040
|
+
const logs = harperProcess.getAndClearLogs();
|
|
2041
|
+
return `Successfully started Harper application with auto-reload in '${effectiveDirectory}' with initial logs:
|
|
2042
|
+
${logs}`;
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
return `Error: failed to start Harper application: ${error}`;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
// tools/harper/stopHarperTool.ts
|
|
2050
|
+
import { tool as tool29 } from "@openai/agents";
|
|
2051
|
+
import { z as z29 } from "zod";
|
|
2052
|
+
var ToolParameters16 = z29.object({});
|
|
2053
|
+
var stopHarperTool = tool29({
|
|
2054
|
+
name: "stopHarperTool",
|
|
2055
|
+
description: "Stops all previously started Harper app background process.",
|
|
2056
|
+
parameters: ToolParameters16,
|
|
2057
|
+
async execute() {
|
|
2058
|
+
if (!harperProcess.running) {
|
|
2059
|
+
return `Error: No Harper application is currently running.`;
|
|
2060
|
+
}
|
|
2061
|
+
try {
|
|
2062
|
+
harperProcess.stop();
|
|
2063
|
+
return `Successfully stopped Harper application.`;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
return `Error stopping Harper application: ${error}`;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
// tools/factory.ts
|
|
2071
|
+
function createTools() {
|
|
2072
|
+
return [
|
|
2073
|
+
changeCwdTool,
|
|
2074
|
+
checkHarperStatusTool,
|
|
2075
|
+
codeInterpreterTool,
|
|
2076
|
+
createApplyPatchTool(),
|
|
2077
|
+
createNewHarperApplicationTool,
|
|
2078
|
+
egrepTool,
|
|
2079
|
+
findTool,
|
|
2080
|
+
getHarperConfigSchemaTool,
|
|
2081
|
+
getHarperResourceInterfaceTool,
|
|
2082
|
+
getHarperSchemaGraphQLTool,
|
|
2083
|
+
getHarperSkillTool,
|
|
2084
|
+
gitAddTool,
|
|
2085
|
+
gitBranchTool,
|
|
2086
|
+
gitCommitTool,
|
|
2087
|
+
gitLogTool,
|
|
2088
|
+
gitStashTool,
|
|
2089
|
+
gitStatusTool,
|
|
2090
|
+
gitWorkspaceTool,
|
|
2091
|
+
hitHarperAPITool,
|
|
2092
|
+
openHarperInBrowserTool,
|
|
2093
|
+
readDirTool,
|
|
2094
|
+
readFileTool,
|
|
2095
|
+
readHarperLogsTool,
|
|
2096
|
+
setInterpreterAutoApproveTool,
|
|
2097
|
+
setPatchAutoApproveTool,
|
|
2098
|
+
setShellAutoApproveTool,
|
|
2099
|
+
shellTool,
|
|
2100
|
+
startHarperTool,
|
|
2101
|
+
stopHarperTool
|
|
2102
|
+
];
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// utils/package/checkForUpdate.ts
|
|
2106
|
+
import chalk10 from "chalk";
|
|
2107
|
+
import spawn3 from "cross-spawn";
|
|
2108
|
+
|
|
2109
|
+
// utils/package/getLatestVersion.ts
|
|
2110
|
+
async function getLatestVersion(packageName) {
|
|
2111
|
+
try {
|
|
2112
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
2113
|
+
signal: AbortSignal.timeout(1e3)
|
|
2114
|
+
// 1 second timeout
|
|
2115
|
+
});
|
|
2116
|
+
if (!response.ok) {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
const data = await response.json();
|
|
2120
|
+
return data.version;
|
|
2121
|
+
} catch {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// utils/package/isVersionNewer.ts
|
|
2127
|
+
function isVersionNewer(latest, current) {
|
|
2128
|
+
const l = latest.split(".").map((x) => parseInt(x, 10));
|
|
2129
|
+
const c = current.split(".").map((x) => parseInt(x, 10));
|
|
2130
|
+
for (let i = 0; i < 3; i++) {
|
|
2131
|
+
let latestNumber = l[i];
|
|
2132
|
+
let currentNumber = c[i];
|
|
2133
|
+
if (latestNumber === void 0 || currentNumber === void 0 || isNaN(latestNumber) || isNaN(currentNumber)) {
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
if (latestNumber > currentNumber) {
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
if (latestNumber < currentNumber) {
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// utils/package/checkForUpdate.ts
|
|
2147
|
+
async function checkForUpdate() {
|
|
2148
|
+
const pkg = getOwnPackageJson();
|
|
2149
|
+
const packageName = pkg.name;
|
|
2150
|
+
const packageVersion = pkg.version;
|
|
2151
|
+
if (process.env.HARPER_AGENT_SKIP_UPDATE) {
|
|
2152
|
+
return packageVersion;
|
|
2153
|
+
}
|
|
2154
|
+
try {
|
|
2155
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
2156
|
+
if (latestVersion && isVersionNewer(latestVersion, packageVersion)) {
|
|
2157
|
+
console.log(
|
|
2158
|
+
chalk10.yellow(
|
|
2159
|
+
`
|
|
2160
|
+
A new version of ${chalk10.bold(packageName)} is available! (${chalk10.dim(packageVersion)} -> ${chalk10.green(latestVersion)})`
|
|
2161
|
+
)
|
|
2162
|
+
);
|
|
2163
|
+
console.log(`Automatically updating to the latest version...
|
|
2164
|
+
`);
|
|
2165
|
+
const lsResult = spawn3.sync("npm", ["cache", "npx", "ls", packageName], {
|
|
2166
|
+
encoding: "utf8"
|
|
2167
|
+
});
|
|
2168
|
+
if (lsResult.stdout) {
|
|
2169
|
+
const keys = lsResult.stdout.split("\n").map((line) => line.trim()).filter((line) => line.includes(":")).filter((line) => {
|
|
2170
|
+
const [, pkgPart] = line.split(":");
|
|
2171
|
+
return pkgPart && pkgPart.trim().startsWith(`${packageName}@`);
|
|
2172
|
+
}).map((line) => line.split(":")[0].trim());
|
|
2173
|
+
if (keys.length > 0) {
|
|
2174
|
+
spawn3.sync("npm", ["cache", "npx", "rm", ...keys], {
|
|
2175
|
+
stdio: "inherit"
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
const result = spawn3.sync("npx", ["-y", `${packageName}@latest`, ...process.argv.slice(2)], {
|
|
2180
|
+
stdio: "inherit"
|
|
2181
|
+
});
|
|
2182
|
+
process.exit(result.status ?? 0);
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
return packageVersion;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// utils/sessions/modelSettings.ts
|
|
2190
|
+
var modelSettings = {
|
|
2191
|
+
parallelToolCalls: false,
|
|
2192
|
+
text: {
|
|
2193
|
+
verbosity: "low"
|
|
2194
|
+
},
|
|
2195
|
+
providerData: {
|
|
2196
|
+
service_tier: trackedState.useFlexTier ? "flex" : "auto"
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
var compactionModelSettings = {
|
|
2200
|
+
...modelSettings,
|
|
2201
|
+
text: {
|
|
2202
|
+
verbosity: "medium"
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
// utils/shell/askQuestion.ts
|
|
2207
|
+
import { createInterface } from "readline/promises";
|
|
2208
|
+
|
|
2209
|
+
// lifecycle/handleExit.ts
|
|
2210
|
+
async function handleExit() {
|
|
2211
|
+
await cleanUpAndSayBye();
|
|
2212
|
+
process.exit(0);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// utils/shell/askQuestion.ts
|
|
2216
|
+
async function askQuestion(query) {
|
|
2217
|
+
const rl = createInterface({
|
|
2218
|
+
input: process.stdin,
|
|
2219
|
+
output: process.stdout
|
|
2220
|
+
});
|
|
2221
|
+
rl.on("SIGINT", handleExit);
|
|
2222
|
+
try {
|
|
2223
|
+
const response = await rl.question(query);
|
|
2224
|
+
console.log("");
|
|
2225
|
+
return response;
|
|
2226
|
+
} finally {
|
|
2227
|
+
rl.close();
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// utils/shell/ensureApiKey.ts
|
|
2232
|
+
import chalk11 from "chalk";
|
|
2233
|
+
|
|
2234
|
+
// utils/arrays/excludeFalsy.ts
|
|
2235
|
+
function excludeFalsy(item) {
|
|
2236
|
+
return !!item;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// utils/shell/askSecureQuestion.ts
|
|
2240
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2241
|
+
import { Writable } from "stream";
|
|
2242
|
+
async function askSecureQuestion(query) {
|
|
2243
|
+
let muted = false;
|
|
2244
|
+
const mutableStdout = new Writable({
|
|
2245
|
+
write: function(chunk, encoding, callback) {
|
|
2246
|
+
if (!muted) {
|
|
2247
|
+
process.stdout.write(chunk, encoding);
|
|
2248
|
+
}
|
|
2249
|
+
callback();
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
const rl = createInterface2({
|
|
2253
|
+
input: process.stdin,
|
|
2254
|
+
output: mutableStdout,
|
|
2255
|
+
terminal: true
|
|
2256
|
+
});
|
|
2257
|
+
rl.on("SIGINT", handleExit);
|
|
2258
|
+
try {
|
|
2259
|
+
const responsePromise = rl.question(query);
|
|
2260
|
+
muted = true;
|
|
2261
|
+
const response = await responsePromise;
|
|
2262
|
+
muted = false;
|
|
2263
|
+
console.log("");
|
|
2264
|
+
return response;
|
|
2265
|
+
} finally {
|
|
2266
|
+
rl.close();
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// utils/shell/ensureApiKey.ts
|
|
2271
|
+
async function ensureApiKey() {
|
|
2272
|
+
const models = [
|
|
2273
|
+
trackedState.model,
|
|
2274
|
+
trackedState.compactionModel
|
|
2275
|
+
].filter(excludeFalsy);
|
|
2276
|
+
const requiredEnvVars = /* @__PURE__ */ new Set();
|
|
2277
|
+
for (const model of models) {
|
|
2278
|
+
if (model.startsWith("claude-")) {
|
|
2279
|
+
requiredEnvVars.add("ANTHROPIC_API_KEY");
|
|
2280
|
+
} else if (model.startsWith("gemini-")) {
|
|
2281
|
+
requiredEnvVars.add("GOOGLE_GENERATIVE_AI_API_KEY");
|
|
2282
|
+
} else if (model.startsWith("ollama-")) {
|
|
2283
|
+
} else {
|
|
2284
|
+
requiredEnvVars.add("OPENAI_API_KEY");
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
for (const envVar of requiredEnvVars) {
|
|
2288
|
+
if (process.env[envVar]) {
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
let providerName = "OpenAI";
|
|
2292
|
+
let keyUrl = "https://platform.openai.com/api-keys";
|
|
2293
|
+
if (envVar === "ANTHROPIC_API_KEY") {
|
|
2294
|
+
providerName = "Anthropic";
|
|
2295
|
+
keyUrl = "https://console.anthropic.com/settings/keys";
|
|
2296
|
+
} else if (envVar === "GOOGLE_GENERATIVE_AI_API_KEY") {
|
|
2297
|
+
providerName = "Google AI";
|
|
2298
|
+
keyUrl = "https://aistudio.google.com/app/apikey";
|
|
2299
|
+
}
|
|
2300
|
+
harperResponse(chalk11.red(`${envVar} is not set.`));
|
|
2301
|
+
console.log(`To get started with ${providerName}, you'll need an API key.`);
|
|
2302
|
+
console.log(`1. Grab a key from ${chalk11.cyan(keyUrl)}`);
|
|
2303
|
+
console.log(`2. Enter it below and I'll save it to your ${chalk11.cyan(".env")} file.
|
|
2304
|
+
`);
|
|
2305
|
+
const key = await askSecureQuestion(`${providerName} API Key: `);
|
|
2306
|
+
if (!key) {
|
|
2307
|
+
console.log(chalk11.red("No key provided. Exiting."));
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
await updateEnv(envVar, key);
|
|
2311
|
+
console.log(chalk11.green("API key saved successfully!\n"));
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// utils/sessions/createSession.ts
|
|
2316
|
+
import { MemorySession as MemorySession3 } from "@openai/agents";
|
|
2317
|
+
|
|
2318
|
+
// lifecycle/trackCompaction.ts
|
|
2319
|
+
import chalk12 from "chalk";
|
|
2320
|
+
function trackCompaction(session) {
|
|
2321
|
+
const originalRunCompaction = session.runCompaction.bind(session);
|
|
2322
|
+
session.runCompaction = async (args) => {
|
|
2323
|
+
const originalMessage = spinner.message;
|
|
2324
|
+
spinner.message = "Compacting conversation history...";
|
|
2325
|
+
const wasSpinning = spinner.isSpinning;
|
|
2326
|
+
let timeout = null;
|
|
2327
|
+
if (!wasSpinning) {
|
|
2328
|
+
if (!trackedState.atStartOfLine) {
|
|
2329
|
+
process.stdout.write("\n");
|
|
2330
|
+
trackedState.atStartOfLine = true;
|
|
2331
|
+
}
|
|
2332
|
+
timeout = setTimeout(() => {
|
|
2333
|
+
spinner.start();
|
|
2334
|
+
}, 50);
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
return await originalRunCompaction(args);
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
const err = error ?? {};
|
|
2340
|
+
const name = err.name || "Error";
|
|
2341
|
+
const message = err.message || String(err);
|
|
2342
|
+
const code = err.code ? ` code=${err.code}` : "";
|
|
2343
|
+
const status = err.status || err.statusCode || err.response?.status;
|
|
2344
|
+
const statusStr = status ? ` status=${status}` : "";
|
|
2345
|
+
const msgLower = (message || "").toLowerCase();
|
|
2346
|
+
const isNoToolOutput = /no tool output found for function call/i.test(message || "");
|
|
2347
|
+
const callIdMatch = typeof message === "string" ? message.match(/function call\s+(call_[A-Za-z0-9_-]+)/i) : null;
|
|
2348
|
+
const callId = callIdMatch?.[1];
|
|
2349
|
+
const isRateLimited = status === 429 || /rate limit|too many requests/i.test(message || "");
|
|
2350
|
+
const isContextExceeded = status === 413 || /maximum context length|context length|too many tokens|token limit|reduce the length|content length/i.test(
|
|
2351
|
+
msgLower
|
|
2352
|
+
);
|
|
2353
|
+
const isAuth = status === 401 || status === 403 || /invalid api key|unauthorized|permission/i.test(msgLower);
|
|
2354
|
+
const isServer = typeof status === "number" && status >= 500 || /server error|temporary|timeout/i.test(msgLower);
|
|
2355
|
+
const hintParts = [];
|
|
2356
|
+
if (isNoToolOutput) {
|
|
2357
|
+
hintParts.push(
|
|
2358
|
+
`A previous tool call in the session history appears to be missing its tool result (orphaned tool call). Compaction cannot reference a tool call without a corresponding tool output. Ensure tools always return a structured object (e.g., { status, output }) and never throw. ` + (callId ? `Missing call id: ${callId}. ` : "") + `If this persists, consider truncating the session before the orphaned call or retrying the failing tool.`
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
if (isContextExceeded) {
|
|
2362
|
+
hintParts.push(
|
|
2363
|
+
`The compaction request likely exceeded the model's context window. Try using a compaction model with a larger context, reducing history, or increasing compaction aggressiveness.`
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
if (isRateLimited) {
|
|
2367
|
+
hintParts.push(
|
|
2368
|
+
`Rate limited by the provider. Back off and retry later, or lower compaction frequency.`
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
if (isAuth) {
|
|
2372
|
+
hintParts.push(`Authentication/permissions issue. Verify the API key and model access for compaction.`);
|
|
2373
|
+
}
|
|
2374
|
+
if (isServer) {
|
|
2375
|
+
hintParts.push(`Upstream server error. This is likely transient; retrying later may succeed.`);
|
|
2376
|
+
}
|
|
2377
|
+
const compactionCtx = `
|
|
2378
|
+
Context: compactionModel=${trackedState.compactionModel || "unknown"}`;
|
|
2379
|
+
let argsSnippet = "";
|
|
2380
|
+
try {
|
|
2381
|
+
const s = typeof args === "string" ? args : JSON.stringify(args);
|
|
2382
|
+
if (s) {
|
|
2383
|
+
argsSnippet = `
|
|
2384
|
+
Compaction args: ${s.slice(0, 300)}${s.length > 300 ? "\u2026" : ""}`;
|
|
2385
|
+
}
|
|
2386
|
+
} catch {
|
|
2387
|
+
}
|
|
2388
|
+
const hint = hintParts.length ? `
|
|
2389
|
+
Hint (compaction): ${hintParts.join(" ")}` : "";
|
|
2390
|
+
let responseDataSnippet = "";
|
|
2391
|
+
const data = err.response?.data ?? err.data;
|
|
2392
|
+
if (data) {
|
|
2393
|
+
try {
|
|
2394
|
+
const s = typeof data === "string" ? data : JSON.stringify(data);
|
|
2395
|
+
responseDataSnippet = `
|
|
2396
|
+
Response data: ${s.slice(0, 500)}${s.length > 500 ? "\u2026" : ""}`;
|
|
2397
|
+
} catch {
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const stack = err.stack ? `
|
|
2401
|
+
Stack: ${String(err.stack).split("\n").slice(0, 8).join("\n")}` : "";
|
|
2402
|
+
const composed = `${name}:${code}${statusStr} ${message}${hint}${compactionCtx}${argsSnippet}${responseDataSnippet}${stack}`;
|
|
2403
|
+
harperResponse(chalk12.red(composed));
|
|
2404
|
+
trackedState.atStartOfLine = true;
|
|
2405
|
+
return void 0;
|
|
2406
|
+
} finally {
|
|
2407
|
+
if (timeout) {
|
|
2408
|
+
clearTimeout(timeout);
|
|
2409
|
+
}
|
|
2410
|
+
if (!wasSpinning) {
|
|
2411
|
+
spinner.stop();
|
|
2412
|
+
}
|
|
2413
|
+
spinner.message = originalMessage;
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// utils/sessions/DiskSession.ts
|
|
2419
|
+
import { MemorySession } from "@openai/agents";
|
|
2420
|
+
import sqlite3 from "sqlite3";
|
|
2421
|
+
var DiskSession = class extends MemorySession {
|
|
2422
|
+
db;
|
|
2423
|
+
ready;
|
|
2424
|
+
useJsonb = false;
|
|
2425
|
+
constructor(dbPath, options) {
|
|
2426
|
+
super(options);
|
|
2427
|
+
this.db = new sqlite3.Database(dbPath);
|
|
2428
|
+
this.ready = this.init(options);
|
|
2429
|
+
}
|
|
2430
|
+
async init(options) {
|
|
2431
|
+
await this.run(`
|
|
2432
|
+
CREATE TABLE IF NOT EXISTS session_items (
|
|
2433
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2434
|
+
sessionId TEXT,
|
|
2435
|
+
data JSONB
|
|
2436
|
+
)
|
|
2437
|
+
`);
|
|
2438
|
+
try {
|
|
2439
|
+
await this.get("SELECT jsonb('{}')");
|
|
2440
|
+
this.useJsonb = true;
|
|
2441
|
+
} catch {
|
|
2442
|
+
this.useJsonb = false;
|
|
2443
|
+
}
|
|
2444
|
+
let sessionId = this.sessionId;
|
|
2445
|
+
if (!options?.sessionId) {
|
|
2446
|
+
const row = await this.get("SELECT DISTINCT sessionId FROM session_items LIMIT 1");
|
|
2447
|
+
if (row) {
|
|
2448
|
+
sessionId = row.sessionId;
|
|
2449
|
+
this.sessionId = sessionId;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
if (!sessionId) {
|
|
2453
|
+
sessionId = this.sessionId || "default-session";
|
|
2454
|
+
}
|
|
2455
|
+
const rows = await this.all(
|
|
2456
|
+
"SELECT json(data) AS data FROM session_items WHERE sessionId = ? ORDER BY id ASC",
|
|
2457
|
+
sessionId
|
|
2458
|
+
);
|
|
2459
|
+
if (rows.length > 0) {
|
|
2460
|
+
const itemsFromDb = rows.map((row) => JSON.parse(row.data));
|
|
2461
|
+
this.items = itemsFromDb;
|
|
2462
|
+
} else {
|
|
2463
|
+
const items = this.items || [];
|
|
2464
|
+
if (items.length > 0) {
|
|
2465
|
+
const sql = this.useJsonb ? "INSERT INTO session_items (sessionId, data) VALUES (?, jsonb(?))" : "INSERT INTO session_items (sessionId, data) VALUES (?, ?)";
|
|
2466
|
+
for (const item of items) {
|
|
2467
|
+
await this.run(sql, sessionId, JSON.stringify(item));
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async getSessionId() {
|
|
2473
|
+
await this.ready;
|
|
2474
|
+
return super.getSessionId();
|
|
2475
|
+
}
|
|
2476
|
+
async getItems(limit) {
|
|
2477
|
+
await this.ready;
|
|
2478
|
+
return super.getItems(limit);
|
|
2479
|
+
}
|
|
2480
|
+
async addItems(items) {
|
|
2481
|
+
await this.ready;
|
|
2482
|
+
await super.addItems(items);
|
|
2483
|
+
const sessionId = await this.getSessionId();
|
|
2484
|
+
const sql = this.useJsonb ? "INSERT INTO session_items (sessionId, data) VALUES (?, jsonb(?))" : "INSERT INTO session_items (sessionId, data) VALUES (?, ?)";
|
|
2485
|
+
for (const item of items) {
|
|
2486
|
+
await this.run(sql, sessionId, JSON.stringify(item));
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
async popItem() {
|
|
2490
|
+
await this.ready;
|
|
2491
|
+
const item = await super.popItem();
|
|
2492
|
+
if (item) {
|
|
2493
|
+
const sessionId = await this.getSessionId();
|
|
2494
|
+
await this.run(
|
|
2495
|
+
`
|
|
2496
|
+
DELETE FROM session_items
|
|
2497
|
+
WHERE id = (
|
|
2498
|
+
SELECT id FROM session_items
|
|
2499
|
+
WHERE sessionId = ?
|
|
2500
|
+
ORDER BY id DESC
|
|
2501
|
+
LIMIT 1
|
|
2502
|
+
)
|
|
2503
|
+
`,
|
|
2504
|
+
sessionId
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
return item;
|
|
2508
|
+
}
|
|
2509
|
+
async clearSession() {
|
|
2510
|
+
await this.ready;
|
|
2511
|
+
await super.clearSession();
|
|
2512
|
+
const sessionId = await this.getSessionId();
|
|
2513
|
+
await this.run("DELETE FROM session_items WHERE sessionId = ?", sessionId);
|
|
2514
|
+
}
|
|
2515
|
+
run(sql, ...params) {
|
|
2516
|
+
return new Promise((resolve2, reject) => {
|
|
2517
|
+
this.db.run(sql, ...params, (err) => {
|
|
2518
|
+
if (err) {
|
|
2519
|
+
reject(err);
|
|
2520
|
+
} else {
|
|
2521
|
+
resolve2();
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
get(sql, ...params) {
|
|
2527
|
+
return new Promise((resolve2, reject) => {
|
|
2528
|
+
this.db.get(sql, ...params, (err, row) => {
|
|
2529
|
+
if (err) {
|
|
2530
|
+
reject(err);
|
|
2531
|
+
} else {
|
|
2532
|
+
resolve2(row);
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
all(sql, ...params) {
|
|
2538
|
+
return new Promise((resolve2, reject) => {
|
|
2539
|
+
this.db.all(sql, ...params, (err, rows) => {
|
|
2540
|
+
if (err) {
|
|
2541
|
+
reject(err);
|
|
2542
|
+
} else {
|
|
2543
|
+
resolve2(rows);
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
|
|
2550
|
+
// utils/sessions/MemoryCompactionSession.ts
|
|
2551
|
+
import {
|
|
2552
|
+
MemorySession as MemorySession2
|
|
2553
|
+
} from "@openai/agents";
|
|
2554
|
+
|
|
2555
|
+
// utils/sessions/compactConversation.ts
|
|
2556
|
+
import { Agent as Agent2, run, system } from "@openai/agents";
|
|
2557
|
+
async function compactConversation(items) {
|
|
2558
|
+
const firstItem = items[0];
|
|
2559
|
+
const recentItems = items.slice(-3);
|
|
2560
|
+
const itemsToCompact = items.slice(1, -3);
|
|
2561
|
+
let noticeContent = "... conversation history compacted ...";
|
|
2562
|
+
if (trackedState.compactionModel && itemsToCompact.length > 0) {
|
|
2563
|
+
try {
|
|
2564
|
+
const agent = new Agent2({
|
|
2565
|
+
name: "History Compactor",
|
|
2566
|
+
model: isOpenAIModel(trackedState.compactionModel) ? trackedState.compactionModel : getModel(trackedState.compactionModel),
|
|
2567
|
+
modelSettings: compactionModelSettings,
|
|
2568
|
+
instructions: "Summarize the conversation history so far into a single concise paragraph. Focus on the key facts and decisions made."
|
|
2569
|
+
});
|
|
2570
|
+
const result = await run(
|
|
2571
|
+
agent,
|
|
2572
|
+
itemsToCompact
|
|
2573
|
+
);
|
|
2574
|
+
const summary = result.finalOutput;
|
|
2575
|
+
if (summary && summary.trim().length > 0) {
|
|
2576
|
+
const s = summary.replace(/\s+/g, " ").trim();
|
|
2577
|
+
noticeContent = `... conversation history compacted: ${s} ...`;
|
|
2578
|
+
}
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
const msg = String(err?.message || err || "");
|
|
2581
|
+
const isNoTrace = /no existing trace found/i.test(msg) || /setCurrentSpan/i.test(msg);
|
|
2582
|
+
if (!isNoTrace) {
|
|
2583
|
+
console.warn("Compaction summarization failed:", msg);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
const itemsToAdd = [firstItem, system(noticeContent), ...recentItems].filter(excludeFalsy);
|
|
2588
|
+
return { noticeContent, itemsToAdd };
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// utils/sessions/modelContextLimits.ts
|
|
2592
|
+
function getModelContextLimit(modelName) {
|
|
2593
|
+
if (!modelName) {
|
|
2594
|
+
return DEFAULT_LIMIT;
|
|
2595
|
+
}
|
|
2596
|
+
const name = modelName.toLowerCase();
|
|
2597
|
+
if (name.startsWith("gpt-4o") || name.startsWith("gpt-5")) {
|
|
2598
|
+
return 2e5;
|
|
2599
|
+
}
|
|
2600
|
+
if (name.startsWith("gpt-4")) {
|
|
2601
|
+
return 128e3;
|
|
2602
|
+
}
|
|
2603
|
+
if (name.startsWith("claude-3.5") || name.startsWith("claude-3")) {
|
|
2604
|
+
return 2e5;
|
|
2605
|
+
}
|
|
2606
|
+
if (name.startsWith("gemini-1.5")) {
|
|
2607
|
+
return 1e6;
|
|
2608
|
+
}
|
|
2609
|
+
if (name.startsWith("gemini-")) {
|
|
2610
|
+
return 128e3;
|
|
2611
|
+
}
|
|
2612
|
+
if (name.startsWith("ollama-")) {
|
|
2613
|
+
return 8e3;
|
|
2614
|
+
}
|
|
2615
|
+
return DEFAULT_LIMIT;
|
|
2616
|
+
}
|
|
2617
|
+
function getCompactionTriggerTokens(modelName, fraction = 0.5) {
|
|
2618
|
+
const limit = getModelContextLimit(modelName);
|
|
2619
|
+
const f = Math.min(Math.max(fraction, 0.5), 0.95);
|
|
2620
|
+
return Math.floor(limit * f);
|
|
2621
|
+
}
|
|
2622
|
+
var DEFAULT_LIMIT = 128e3;
|
|
2623
|
+
|
|
2624
|
+
// utils/sessions/MemoryCompactionSession.ts
|
|
2625
|
+
var MemoryCompactionSession = class {
|
|
2626
|
+
underlyingSession;
|
|
2627
|
+
triggerTokens;
|
|
2628
|
+
itemsAddedSinceLastCompaction = 0;
|
|
2629
|
+
constructor(options) {
|
|
2630
|
+
this.underlyingSession = options.underlyingSession ?? new MemorySession2();
|
|
2631
|
+
if (trackedState.compactionModel) {
|
|
2632
|
+
const fraction = options.triggerFraction ?? 0.5;
|
|
2633
|
+
this.triggerTokens = getCompactionTriggerTokens(trackedState.compactionModel, fraction);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async getSessionId() {
|
|
2637
|
+
return this.underlyingSession.getSessionId();
|
|
2638
|
+
}
|
|
2639
|
+
async getItems(limit) {
|
|
2640
|
+
return this.underlyingSession.getItems(limit);
|
|
2641
|
+
}
|
|
2642
|
+
async addItems(items) {
|
|
2643
|
+
await this.underlyingSession.addItems(items);
|
|
2644
|
+
this.itemsAddedSinceLastCompaction += items.length;
|
|
2645
|
+
await this.runCompaction({ reason: "post-add-check", mode: "auto" });
|
|
2646
|
+
}
|
|
2647
|
+
async popItem() {
|
|
2648
|
+
return this.underlyingSession.popItem();
|
|
2649
|
+
}
|
|
2650
|
+
async clearSession() {
|
|
2651
|
+
this.itemsAddedSinceLastCompaction = 0;
|
|
2652
|
+
await this.underlyingSession.clearSession();
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Compaction entry point used by the underlying agent.
|
|
2656
|
+
* This method first decides if compaction is necessary (token-aware gating),
|
|
2657
|
+
* and only then performs the compaction. External callers should invoke this
|
|
2658
|
+
* method directly; there is no separate "maybe" helper anymore.
|
|
2659
|
+
*
|
|
2660
|
+
* Behavior:
|
|
2661
|
+
* - If a token trigger threshold is configured (via modelName), compaction is
|
|
2662
|
+
* skipped unless the estimated token count exceeds the threshold, unless a
|
|
2663
|
+
* forcing flag is provided in args.
|
|
2664
|
+
* - If history is trivially small (<= 4 items), it skips compaction.
|
|
2665
|
+
* - Otherwise, it keeps the first item, adds a compaction notice (optionally
|
|
2666
|
+
* summarized by the model), and retains the last 3 recent items.
|
|
2667
|
+
*/
|
|
2668
|
+
async runCompaction(args) {
|
|
2669
|
+
const items = await this.underlyingSession.getItems();
|
|
2670
|
+
if (items.length <= 1) {
|
|
2671
|
+
return null;
|
|
2672
|
+
}
|
|
2673
|
+
const force = !!args?.force || !!args?.always || args?.trigger === "force";
|
|
2674
|
+
if (!force && this.triggerTokens && this.triggerTokens > 0) {
|
|
2675
|
+
const tokenEstimate = estimateTokens(items);
|
|
2676
|
+
if (tokenEstimate < this.triggerTokens) {
|
|
2677
|
+
return null;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (items.length <= 4) {
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
const { itemsToAdd } = await compactConversation(items);
|
|
2684
|
+
this.itemsAddedSinceLastCompaction = 0;
|
|
2685
|
+
await this.underlyingSession.clearSession();
|
|
2686
|
+
await this.underlyingSession.addItems(itemsToAdd.filter(excludeFalsy));
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
function estimateTokens(items) {
|
|
2691
|
+
let chars = 0;
|
|
2692
|
+
for (const it of items) {
|
|
2693
|
+
if (!it) {
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
if (Array.isArray(it.content)) {
|
|
2697
|
+
for (const c of it.content) {
|
|
2698
|
+
if (!c) {
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
if (typeof c.text === "string") {
|
|
2702
|
+
chars += c.text.length;
|
|
2703
|
+
} else if (typeof c.content === "string") {
|
|
2704
|
+
chars += c.content.length;
|
|
2705
|
+
} else if (typeof c === "string") {
|
|
2706
|
+
chars += c.length;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
if (typeof it.content === "string") {
|
|
2711
|
+
chars += it.content.length;
|
|
2712
|
+
}
|
|
2713
|
+
if (typeof it.text === "string") {
|
|
2714
|
+
chars += it.text.length;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return Math.ceil(chars / 4);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// utils/sessions/createSession.ts
|
|
2721
|
+
function createSession(sessionPath = null) {
|
|
2722
|
+
const underlyingSession = sessionPath ? new DiskSession(sessionPath) : new MemorySession3();
|
|
2723
|
+
const session = new MemoryCompactionSession({
|
|
2724
|
+
underlyingSession
|
|
2725
|
+
});
|
|
2726
|
+
trackCompaction(session);
|
|
2727
|
+
return session;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// agent.ts
|
|
2731
|
+
var argumentTruncationPoint = 100;
|
|
2732
|
+
process.on("SIGINT", handleExit);
|
|
2733
|
+
process.on("SIGTERM", handleExit);
|
|
2734
|
+
async function main() {
|
|
2735
|
+
process.on("SIGINT", async () => {
|
|
2736
|
+
await cleanUpAndSayBye();
|
|
2737
|
+
process.exit(0);
|
|
2738
|
+
});
|
|
2739
|
+
await checkForUpdate();
|
|
2740
|
+
parseArgs();
|
|
2741
|
+
await ensureApiKey();
|
|
2742
|
+
sayHi();
|
|
2743
|
+
const agent = trackedState.agent = new Agent3({
|
|
2744
|
+
name: "Harper App Development Assistant",
|
|
2745
|
+
model: isOpenAIModel(trackedState.model) ? trackedState.model : getModel(trackedState.model),
|
|
2746
|
+
modelSettings,
|
|
2747
|
+
instructions: readAgentsMD() || defaultInstructions(),
|
|
2748
|
+
tools: createTools()
|
|
2749
|
+
});
|
|
2750
|
+
const session = createSession(trackedState.sessionPath);
|
|
2751
|
+
while (true) {
|
|
2752
|
+
let task = "";
|
|
2753
|
+
let lastToolCallInfo = null;
|
|
2754
|
+
trackedState.controller = new AbortController();
|
|
2755
|
+
if (!trackedState.approvalState) {
|
|
2756
|
+
task = await askQuestion("> ");
|
|
2757
|
+
if (!task) {
|
|
2758
|
+
trackedState.emptyLines += 1;
|
|
2759
|
+
if (trackedState.emptyLines >= 2) {
|
|
2760
|
+
return handleExit();
|
|
2761
|
+
}
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
trackedState.emptyLines = 0;
|
|
2765
|
+
process.stdout.write("\n");
|
|
2766
|
+
}
|
|
2767
|
+
spinner.start();
|
|
2768
|
+
try {
|
|
2769
|
+
const stream = await run2(agent, trackedState.approvalState ?? task, {
|
|
2770
|
+
session,
|
|
2771
|
+
stream: true,
|
|
2772
|
+
signal: trackedState.controller.signal,
|
|
2773
|
+
maxTurns: 30
|
|
2774
|
+
});
|
|
2775
|
+
trackedState.approvalState = null;
|
|
2776
|
+
let hasStartedResponse = false;
|
|
2777
|
+
trackedState.atStartOfLine = true;
|
|
2778
|
+
for await (const event of stream) {
|
|
2779
|
+
switch (event.type) {
|
|
2780
|
+
case "raw_model_stream_event":
|
|
2781
|
+
const data = event.data;
|
|
2782
|
+
switch (data.type) {
|
|
2783
|
+
case "response_started":
|
|
2784
|
+
if (!trackedState.atStartOfLine) {
|
|
2785
|
+
process.stdout.write("\n");
|
|
2786
|
+
trackedState.atStartOfLine = true;
|
|
2787
|
+
}
|
|
2788
|
+
spinner.start();
|
|
2789
|
+
break;
|
|
2790
|
+
case "output_text_delta":
|
|
2791
|
+
spinner.stop();
|
|
2792
|
+
if (!hasStartedResponse) {
|
|
2793
|
+
process.stdout.write(`${chalk13.bold("Harper:")} `);
|
|
2794
|
+
hasStartedResponse = true;
|
|
2795
|
+
}
|
|
2796
|
+
process.stdout.write(chalk13.cyan(data.delta));
|
|
2797
|
+
trackedState.atStartOfLine = data.delta.endsWith("\n");
|
|
2798
|
+
break;
|
|
2799
|
+
case "response_done":
|
|
2800
|
+
const tier = data.response?.providerData?.service_tier || data.providerData?.service_tier;
|
|
2801
|
+
if (tier) {
|
|
2802
|
+
stream.state.usage.serviceTier = tier;
|
|
2803
|
+
const entries = stream.state.usage.requestUsageEntries;
|
|
2804
|
+
if (entries && entries.length > 0) {
|
|
2805
|
+
entries[entries.length - 1].serviceTier = tier;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
spinner.stop();
|
|
2809
|
+
trackedState.atStartOfLine = true;
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
break;
|
|
2813
|
+
case "agent_updated_stream_event":
|
|
2814
|
+
spinner.stop();
|
|
2815
|
+
console.log(
|
|
2816
|
+
`
|
|
2817
|
+
${chalk13.magenta("\u{1F464}")} ${chalk13.bold("Agent switched to:")} ${chalk13.italic(event.agent.name)}`
|
|
2818
|
+
);
|
|
2819
|
+
trackedState.atStartOfLine = true;
|
|
2820
|
+
spinner.start();
|
|
2821
|
+
break;
|
|
2822
|
+
case "run_item_stream_event":
|
|
2823
|
+
if (event.name === "tool_called") {
|
|
2824
|
+
spinner.stop();
|
|
2825
|
+
const item = event.item.rawItem ?? event.item;
|
|
2826
|
+
const name = item.name || item.type || "tool";
|
|
2827
|
+
let args = typeof item.arguments === "string" ? item.arguments : item.arguments ? JSON.stringify(item.arguments) : "";
|
|
2828
|
+
if (!args && item.type === "shell_call" && item.action?.commands) {
|
|
2829
|
+
args = JSON.stringify(item.action.commands);
|
|
2830
|
+
}
|
|
2831
|
+
if (!args && item.type === "apply_patch_call" && item.operation) {
|
|
2832
|
+
args = JSON.stringify(item.operation);
|
|
2833
|
+
}
|
|
2834
|
+
const displayedArgs = args ? `(${args.slice(0, argumentTruncationPoint)}${args.length > argumentTruncationPoint ? "..." : ""})` : "()";
|
|
2835
|
+
console.log(`
|
|
2836
|
+
${chalk13.yellow("\u{1F6E0}\uFE0F")} ${chalk13.cyan(name)}${chalk13.dim(displayedArgs)}`);
|
|
2837
|
+
lastToolCallInfo = `${name}${displayedArgs}`;
|
|
2838
|
+
trackedState.atStartOfLine = true;
|
|
2839
|
+
if (!stream.interruptions?.length) {
|
|
2840
|
+
spinner.start();
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
spinner.status = costTracker.getStatusString(
|
|
2846
|
+
stream.state.usage,
|
|
2847
|
+
trackedState.model || "gpt-5.2",
|
|
2848
|
+
trackedState.compactionModel || "gpt-4o-mini"
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
if (stream.interruptions?.length) {
|
|
2852
|
+
for (const interruption of stream.interruptions) {
|
|
2853
|
+
spinner.stop();
|
|
2854
|
+
console.log(
|
|
2855
|
+
chalk13.bold.bgYellow.black("\nTool approval required (see above):")
|
|
2856
|
+
);
|
|
2857
|
+
const answer = await askQuestion(` Proceed? [y/N] `);
|
|
2858
|
+
const approved = answer.trim().toLowerCase();
|
|
2859
|
+
const ok = approved === "y" || approved === "yes" || approved === "ok" || approved === "k";
|
|
2860
|
+
if (ok) {
|
|
2861
|
+
stream.state.approve(interruption);
|
|
2862
|
+
} else {
|
|
2863
|
+
stream.state.reject(interruption);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
trackedState.approvalState = stream.state;
|
|
2867
|
+
}
|
|
2868
|
+
spinner.stop();
|
|
2869
|
+
if (!trackedState.atStartOfLine || hasStartedResponse) {
|
|
2870
|
+
process.stdout.write("\n\n");
|
|
2871
|
+
}
|
|
2872
|
+
if (!trackedState.approvalState) {
|
|
2873
|
+
costTracker.recordTurn(
|
|
2874
|
+
trackedState.model || "gpt-5.2",
|
|
2875
|
+
stream.state.usage,
|
|
2876
|
+
trackedState.compactionModel || "gpt-4o-mini"
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
spinner.stop();
|
|
2881
|
+
process.stdout.write("\n");
|
|
2882
|
+
const err = error ?? {};
|
|
2883
|
+
const name = err.name || "Error";
|
|
2884
|
+
const message = err.message || String(err);
|
|
2885
|
+
const code = err.code ? ` code=${err.code}` : "";
|
|
2886
|
+
const status = err.status || err.statusCode || err.response?.status;
|
|
2887
|
+
const statusStr = status ? ` status=${status}` : "";
|
|
2888
|
+
const callIdMatch = message.match(/function call\s+(call_[A-Za-z0-9_-]+)/i);
|
|
2889
|
+
const callId = callIdMatch?.[1];
|
|
2890
|
+
const isNoToolOutput = /No tool output found for function call/i.test(message || "");
|
|
2891
|
+
const hint = isNoToolOutput ? `
|
|
2892
|
+
Hint: A tool likely threw or returned no result. Ensure tools always return a structured object (e.g., { status, output }) and never throw. If this followed a tool call${callId ? ` (${callId})` : ""}${lastToolCallInfo ? `: ${lastToolCallInfo}` : ""}, review that tool's implementation and logs.` : "";
|
|
2893
|
+
let responseDataSnippet = "";
|
|
2894
|
+
const data = err.response?.data ?? err.data;
|
|
2895
|
+
if (data) {
|
|
2896
|
+
try {
|
|
2897
|
+
const s = typeof data === "string" ? data : JSON.stringify(data);
|
|
2898
|
+
responseDataSnippet = `
|
|
2899
|
+
Response data: ${s.slice(0, 500)}${s.length > 500 ? "\u2026" : ""}`;
|
|
2900
|
+
} catch {
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const stack = err.stack ? `
|
|
2904
|
+
Stack: ${String(err.stack).split("\n").slice(0, 8).join("\n")}` : "";
|
|
2905
|
+
const lastTool = lastToolCallInfo ? `
|
|
2906
|
+
Last tool call: ${lastToolCallInfo}` : "";
|
|
2907
|
+
const composed = `${name}:${code}${statusStr} ${message}${hint}${lastTool}${responseDataSnippet}${stack}`;
|
|
2908
|
+
harperResponse(chalk13.red(composed));
|
|
2909
|
+
trackedState.atStartOfLine = true;
|
|
2910
|
+
trackedState.approvalState = null;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
main().catch((error) => {
|
|
2915
|
+
console.error(error);
|
|
2916
|
+
process.exitCode = 1;
|
|
2917
|
+
});
|