@harperfast/agent 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/agent.js +140 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,18 @@ Harper: What do you want to do together today?
56
56
  >
57
57
  ```
58
58
 
59
+ ### Non-interactive: pipe an initial prompt
60
+
61
+ You can pass an initial chat dump via stdin. This runs a one-shot interaction and exits after responding:
62
+
63
+ ```bash
64
+ cat somePrompt.md | harper-agent
65
+ # or
66
+ harper-agent < somePrompt.md
67
+ ```
68
+
69
+ In this mode, the initial greeting question is suppressed, and the agent processes the provided prompt immediately.
70
+
59
71
  ## Model Selection
60
72
 
61
73
  By default, `harper-agent` uses OpenAI. You can switch to other models using the `--model` (or `-m`) flag:
package/dist/agent.js CHANGED
@@ -92,6 +92,17 @@ var CostTracker = class {
92
92
  this.totalCompactionCost = 0;
93
93
  this.hasUnknownPrices = false;
94
94
  }
95
+ getTotalCost() {
96
+ return this.totalCost + this.totalCompactionCost;
97
+ }
98
+ getEstimatedTotalCost(currentTurnUsage, model, compactionModel) {
99
+ const { turnCost, compactionCost } = this.calculateUsageCosts(
100
+ model,
101
+ currentTurnUsage,
102
+ compactionModel
103
+ );
104
+ return this.getTotalCost() + turnCost + compactionCost;
105
+ }
95
106
  recordTurn(model, usage, compactionModel) {
96
107
  const { turnCost, compactionCost, unknownPrices } = this.calculateUsageCosts(model, usage, compactionModel);
97
108
  this.totalInputTokens += usage.inputTokens;
@@ -315,6 +326,8 @@ var trackedState = {
315
326
  useFlexTier: false,
316
327
  disableSpinner: false,
317
328
  enableInterruptions: true,
329
+ maxTurns: 30,
330
+ maxCost: null,
318
331
  session: null
319
332
  };
320
333
 
@@ -516,6 +529,12 @@ ${chalk3.bold("OPTIONS")}
516
529
  Can also be set via HARPER_AGENT_COMPACTION_MODEL environment variable.
517
530
  -s, --session Specify a path to a SQLite database file to persist the chat session.
518
531
  Can also be set via HARPER_AGENT_SESSION environment variable.
532
+ --max-turns Specify the maximum number of turns for the agent run.
533
+ In task-driven mode, this defaults to unlimited.
534
+ Can also be set via HARPER_AGENT_MAX_TURNS environment variable.
535
+ --max-cost Specify the maximum cost (in USD) for the agent run.
536
+ If exceeded, the agent will exit with a non-zero code.
537
+ Can also be set via HARPER_AGENT_MAX_COST environment variable.
519
538
  --flex-tier Force the use of the flex service tier for lower costs but potentially
520
539
  more errors under high system load.
521
540
  Can also be set via HARPER_AGENT_FLEX_TIER=true environment variable.
@@ -583,19 +602,31 @@ function parseArgs() {
583
602
  const flagPairs = [
584
603
  ["model", ["--model", "-m", "model"]],
585
604
  ["compactionModel", ["--compaction-model", "-c", "compaction-model"]],
586
- ["sessionPath", ["--session", "-s", "session"]]
605
+ ["sessionPath", ["--session", "-s", "session"]],
606
+ ["maxTurns", ["--max-turns"]],
607
+ ["maxCost", ["--max-cost"]]
587
608
  ];
588
609
  let handled = false;
589
610
  for (const [key, prefixes] of flagPairs) {
590
611
  for (const prefix of prefixes) {
591
612
  if (arg === prefix) {
592
613
  if (args[i + 1]) {
593
- trackedState[key] = stripQuotes(args[++i]);
614
+ const val = stripQuotes(args[++i]);
615
+ if (key === "maxTurns" || key === "maxCost") {
616
+ trackedState[key] = parseFloat(val);
617
+ } else {
618
+ trackedState[key] = val;
619
+ }
594
620
  }
595
621
  handled = true;
596
622
  break;
597
623
  } else if (arg.startsWith(`${prefix}=`)) {
598
- trackedState[key] = stripQuotes(arg.slice(prefix.length + 1));
624
+ const val = stripQuotes(arg.slice(prefix.length + 1));
625
+ if (key === "maxTurns" || key === "maxCost") {
626
+ trackedState[key] = parseFloat(val);
627
+ } else {
628
+ trackedState[key] = val;
629
+ }
599
630
  handled = true;
600
631
  break;
601
632
  }
@@ -631,6 +662,12 @@ function parseArgs() {
631
662
  if (!trackedState.sessionPath && process.env.HARPER_AGENT_SESSION) {
632
663
  trackedState.sessionPath = process.env.HARPER_AGENT_SESSION;
633
664
  }
665
+ if (process.env.HARPER_AGENT_MAX_TURNS) {
666
+ trackedState.maxTurns = parseFloat(process.env.HARPER_AGENT_MAX_TURNS);
667
+ }
668
+ if (process.env.HARPER_AGENT_MAX_COST) {
669
+ trackedState.maxCost = parseFloat(process.env.HARPER_AGENT_MAX_COST);
670
+ }
634
671
  const sp = trackedState.sessionPath;
635
672
  if (sp) {
636
673
  trackedState.sessionPath = sp && !sp.startsWith("~") && !path.isAbsolute(sp) ? path.resolve(process.cwd(), sp) : sp;
@@ -1231,12 +1268,12 @@ async function requiredSkillForOperation(path8, type) {
1231
1268
  }
1232
1269
  const p = normalizedPath(path8);
1233
1270
  const read = await getSkillsRead();
1234
- if (p.includes("/resources/") || p.startsWith("resources/")) {
1235
- if (!read.includes("custom-resources") && !read.includes("extending-tables")) {
1236
- return pickExistingSkill(["custom-resources", "extending-tables"]);
1271
+ if (p.includes("/resources/") || p.startsWith("resources/") || p.endsWith("resources.ts") || p.endsWith("resources.js")) {
1272
+ if (!read.includes("automatic-apis")) {
1273
+ return pickExistingSkill(["automatic-apis"]);
1237
1274
  }
1238
1275
  }
1239
- if (p.includes("/schemas/") || p.startsWith("schemas/") || p.includes("/schema/") || p.startsWith("schema/")) {
1276
+ if (p.endsWith(".graphql")) {
1240
1277
  if (!read.includes("adding-tables-with-schemas")) {
1241
1278
  return pickExistingSkill(["adding-tables-with-schemas"]);
1242
1279
  }
@@ -1275,40 +1312,42 @@ ${chalk7.bold.bgYellow.black(" Apply patch approval required: ")}`);
1275
1312
  return false;
1276
1313
  }
1277
1314
  }
1315
+ async function execute11(operation) {
1316
+ try {
1317
+ const needed = await requiredSkillForOperation(operation.path, operation.type);
1318
+ if (needed) {
1319
+ const content = await execute10({ skill: needed });
1320
+ console.log(`Understanding ${needed} is necessary before applying this patch.`);
1321
+ return { status: "failed, skill guarded", output: content };
1322
+ }
1323
+ switch (operation.type) {
1324
+ case "create_file":
1325
+ if (!operation.diff) {
1326
+ return { status: "failed", output: "Error: diff is required for create_file" };
1327
+ }
1328
+ return await editor.createFile(operation);
1329
+ case "update_file":
1330
+ if (!operation.diff) {
1331
+ return { status: "failed", output: "Error: diff is required for update_file" };
1332
+ }
1333
+ return await editor.updateFile(operation);
1334
+ case "delete_file":
1335
+ return await editor.deleteFile(operation);
1336
+ default:
1337
+ return { status: "failed", output: `Error: Unknown operation type: ${operation.type}` };
1338
+ }
1339
+ } catch (err) {
1340
+ console.error("hit unexpected error in apply patch tool", err);
1341
+ return { status: "failed", output: `apply_patch threw: ${String(err)}` };
1342
+ }
1343
+ }
1278
1344
  function createApplyPatchTool() {
1279
1345
  return tool11({
1280
1346
  name: "apply_patch",
1281
1347
  description: "Applies a patch (create, update, or delete a file) to the workspace.",
1282
1348
  parameters: ApplyPatchParameters,
1283
1349
  needsApproval,
1284
- execute: async (operation) => {
1285
- try {
1286
- const needed = await requiredSkillForOperation(operation.path, operation.type);
1287
- if (needed) {
1288
- const content = await execute10({ skill: needed });
1289
- return { status: "completed", output: content };
1290
- }
1291
- switch (operation.type) {
1292
- case "create_file":
1293
- if (!operation.diff) {
1294
- return { status: "failed", output: "Error: diff is required for create_file" };
1295
- }
1296
- return await editor.createFile(operation);
1297
- case "update_file":
1298
- if (!operation.diff) {
1299
- return { status: "failed", output: "Error: diff is required for update_file" };
1300
- }
1301
- return await editor.updateFile(operation);
1302
- case "delete_file":
1303
- return await editor.deleteFile(operation);
1304
- default:
1305
- return { status: "failed", output: `Error: Unknown operation type: ${operation.type}` };
1306
- }
1307
- } catch (err) {
1308
- console.error("hit unexpected error in apply patch tool", err);
1309
- return { status: "failed", output: `apply_patch threw: ${String(err)}` };
1310
- }
1311
- }
1350
+ execute: execute11
1312
1351
  });
1313
1352
  }
1314
1353
 
@@ -1319,7 +1358,7 @@ import { z as z12 } from "zod";
1319
1358
  var ToolParameters11 = z12.object({
1320
1359
  path: z12.string().describe("Directory to switch into. Can be absolute or relative to current workspace.")
1321
1360
  });
1322
- async function execute11({ path: path8 }) {
1361
+ async function execute12({ path: path8 }) {
1323
1362
  try {
1324
1363
  const target = resolvePath(trackedState.cwd, path8);
1325
1364
  const stat = statSync(target);
@@ -1350,7 +1389,7 @@ var changeCwdTool = tool12({
1350
1389
  name: "change_cwd",
1351
1390
  description: "Changes the current working directory for subsequent tools. Accepts absolute or relative paths.",
1352
1391
  parameters: ToolParameters11,
1353
- execute: execute11
1392
+ execute: execute12
1354
1393
  });
1355
1394
 
1356
1395
  // tools/files/egrepTool.ts
@@ -1457,7 +1496,7 @@ var IMAGE_EXTENSIONS = {
1457
1496
  ".webp": "image/webp",
1458
1497
  ".bmp": "image/bmp"
1459
1498
  };
1460
- async function execute12({ fileName }) {
1499
+ async function execute13({ fileName }) {
1461
1500
  try {
1462
1501
  const normalized = String(fileName).replace(/\\/g, "/");
1463
1502
  const m = normalized.match(/(?:^|\/)skills\/([A-Za-z0-9_-]+)\.md(?:$|[?#])/);
@@ -1483,7 +1522,7 @@ var readFileTool = tool16({
1483
1522
  name: "read_file",
1484
1523
  description: "Reads the contents of a specified file. If the file is an image, it returns a structured image object.",
1485
1524
  parameters: ToolParameters15,
1486
- execute: execute12
1525
+ execute: execute13
1487
1526
  });
1488
1527
 
1489
1528
  // tools/general/codeInterpreterTool.ts
@@ -1505,7 +1544,7 @@ var codeInterpreterTool = tool17({
1505
1544
  name: "code_interpreter",
1506
1545
  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.",
1507
1546
  parameters: CodeInterpreterParameters,
1508
- execute: execute13,
1547
+ execute: execute14,
1509
1548
  needsApproval: needsApproval2
1510
1549
  });
1511
1550
  async function needsApproval2(runContext, { code, language }, callId) {
@@ -1527,7 +1566,7 @@ ${chalk8.bold.bgYellow.black(` Code interpreter (${language}) approval required:
1527
1566
  }
1528
1567
  return !autoApproved;
1529
1568
  }
1530
- async function execute13({ code, language }) {
1569
+ async function execute14({ code, language }) {
1531
1570
  const extension = language === "javascript" ? "js" : "py";
1532
1571
  const interpreter = language === "javascript" ? "node" : "python3";
1533
1572
  const tempFile = path6.join(trackedState.cwd, `.temp_code_${Date.now()}.${extension}`);
@@ -2104,7 +2143,7 @@ var ToolParameters17 = z30.object({
2104
2143
  directoryName: z30.string().describe("The name of the directory to create the application in."),
2105
2144
  template: z30.enum(["vanilla-ts", "vanilla", "react-ts", "react"]).optional().describe("The template to use for the new application. Defaults to vanilla-ts.").default("vanilla-ts")
2106
2145
  });
2107
- async function execute14({ directoryName, template }) {
2146
+ async function execute15({ directoryName, template }) {
2108
2147
  const currentCwd = trackedState.cwd;
2109
2148
  const resolvedPath = resolvePath(currentCwd, directoryName);
2110
2149
  const isCurrentDir = resolvedPath === currentCwd;
@@ -2121,7 +2160,7 @@ async function execute14({ directoryName, template }) {
2121
2160
  });
2122
2161
  console.log(`Initializing new Git repository in ${resolvedPath}...`);
2123
2162
  execSync3("git init", { cwd: resolvedPath, stdio: "ignore" });
2124
- const switchedDir = await execute11({ path: resolvedPath });
2163
+ const switchedDir = await execute12({ path: resolvedPath });
2125
2164
  return `Successfully created a new Harper application in '${resolvedPath}' using template '${template}' with a matching Git repository initialized. Use the readDir and readFile tools to inspect the contents of the application. ${switchedDir}.`;
2126
2165
  } catch (error) {
2127
2166
  let errorMsg = `Error creating new Harper application: ${error.message}`;
@@ -2138,7 +2177,7 @@ var createNewHarperApplicationTool = tool30({
2138
2177
  name: "create_new_harper_application",
2139
2178
  description: "Creates a new Harper application using the best available package manager (yarn/pnpm/bun/deno, falling back to npm).",
2140
2179
  parameters: ToolParameters17,
2141
- execute: execute14
2180
+ execute: execute15
2142
2181
  });
2143
2182
 
2144
2183
  // tools/harper/getHarperConfigSchemaTool.ts
@@ -2671,6 +2710,19 @@ async function ensureApiKey() {
2671
2710
  }
2672
2711
  }
2673
2712
 
2713
+ // utils/shell/getStdin.ts
2714
+ async function getStdin() {
2715
+ if (process.stdin.isTTY) {
2716
+ return "";
2717
+ }
2718
+ let result = "";
2719
+ process.stdin.setEncoding("utf8");
2720
+ for await (const chunk of process.stdin) {
2721
+ result += chunk;
2722
+ }
2723
+ return result.trim();
2724
+ }
2725
+
2674
2726
  // utils/sessions/createSession.ts
2675
2727
  import { MemorySession as MemorySession3 } from "@openai/agents";
2676
2728
 
@@ -3115,6 +3167,7 @@ async function main() {
3115
3167
  parseArgs();
3116
3168
  await ensureApiKey();
3117
3169
  sayHi();
3170
+ const stdinPrompt = await getStdin();
3118
3171
  const agent = trackedState.agent = new Agent2({
3119
3172
  name: "Harper App Development Assistant",
3120
3173
  model: isOpenAIModel(trackedState.model) ? trackedState.model : getModel(trackedState.model),
@@ -3123,12 +3176,20 @@ async function main() {
3123
3176
  tools: createTools()
3124
3177
  });
3125
3178
  const session = trackedState.session = createSession(trackedState.sessionPath);
3179
+ let firstIteration = true;
3126
3180
  while (true) {
3127
3181
  let task = "";
3128
3182
  let lastToolCallInfo = null;
3129
3183
  trackedState.controller = new AbortController();
3130
3184
  if (!trackedState.approvalState) {
3131
- task = await askQuestion("> ");
3185
+ if (firstIteration && stdinPrompt) {
3186
+ task = stdinPrompt;
3187
+ console.log(`${chalk13.bold(">")} ${task}
3188
+ `);
3189
+ } else {
3190
+ task = await askQuestion("> ");
3191
+ }
3192
+ firstIteration = false;
3132
3193
  if (!task) {
3133
3194
  trackedState.emptyLines += 1;
3134
3195
  if (trackedState.emptyLines >= 2) {
@@ -3145,7 +3206,7 @@ async function main() {
3145
3206
  session,
3146
3207
  stream: true,
3147
3208
  signal: trackedState.controller.signal,
3148
- maxTurns: 30
3209
+ maxTurns: trackedState.maxTurns
3149
3210
  });
3150
3211
  trackedState.approvalState = null;
3151
3212
  let hasStartedResponse = false;
@@ -3222,6 +3283,26 @@ ${chalk13.yellow("\u{1F6E0}\uFE0F")} ${chalk13.cyan(name)}${chalk13.dim(display
3222
3283
  trackedState.model || "gpt-5.2",
3223
3284
  trackedState.compactionModel || "gpt-4o-mini"
3224
3285
  );
3286
+ if (trackedState.maxCost !== null) {
3287
+ const estimatedTotalCost = costTracker.getEstimatedTotalCost(
3288
+ stream.state.usage,
3289
+ trackedState.model || "gpt-5.2",
3290
+ trackedState.compactionModel || "gpt-4o-mini"
3291
+ );
3292
+ if (estimatedTotalCost > trackedState.maxCost) {
3293
+ spinner.stop();
3294
+ console.log(
3295
+ chalk13.red(
3296
+ `Cost limit exceeded: $${estimatedTotalCost.toFixed(4)} > $${trackedState.maxCost.toFixed(4)}`
3297
+ )
3298
+ );
3299
+ if (trackedState.controller) {
3300
+ trackedState.controller.abort();
3301
+ }
3302
+ process.exitCode = 1;
3303
+ return handleExit();
3304
+ }
3305
+ }
3225
3306
  }
3226
3307
  if (stream.interruptions?.length) {
3227
3308
  for (const interruption of stream.interruptions) {
@@ -3250,6 +3331,19 @@ ${chalk13.yellow("\u{1F6E0}\uFE0F")} ${chalk13.cyan(name)}${chalk13.dim(display
3250
3331
  stream.state.usage,
3251
3332
  trackedState.compactionModel || "gpt-4o-mini"
3252
3333
  );
3334
+ if (trackedState.maxCost !== null && costTracker.getTotalCost() > trackedState.maxCost) {
3335
+ spinner.stop();
3336
+ console.log(
3337
+ chalk13.red(
3338
+ `Cost limit exceeded: $${costTracker.getTotalCost().toFixed(4)} > $${trackedState.maxCost.toFixed(4)}`
3339
+ )
3340
+ );
3341
+ process.exitCode = 1;
3342
+ return handleExit();
3343
+ }
3344
+ }
3345
+ if (stdinPrompt) {
3346
+ return handleExit();
3253
3347
  }
3254
3348
  } catch (error) {
3255
3349
  spinner.stop();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@harperfast/agent",
3
3
  "description": "AI to help you with Harper app management",
4
- "version": "0.13.1",
4
+ "version": "0.13.3",
5
5
  "main": "dist/agent.js",
6
6
  "repository": "github:HarperFast/harper-agent",
7
7
  "bugs": {