stackai 0.1.2 → 0.1.4

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 (2) hide show
  1. package/dist/cli.js +268 -38
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -173,6 +173,40 @@ function toLLMError(err) {
173
173
  // ../core/dist/file-agent.js
174
174
  import { promises as fs } from "fs";
175
175
  import path from "path";
176
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
177
+ "node_modules",
178
+ ".git",
179
+ "dist",
180
+ ".next",
181
+ ".turbo",
182
+ "build",
183
+ "out",
184
+ "coverage",
185
+ ".cache"
186
+ ]);
187
+ function globToRegExp(glob) {
188
+ let re = "";
189
+ for (let i = 0; i < glob.length; i++) {
190
+ const c = glob[i];
191
+ if (c === "*") {
192
+ if (glob[i + 1] === "*") {
193
+ re += ".*";
194
+ i++;
195
+ if (glob[i + 1] === "/")
196
+ i++;
197
+ } else {
198
+ re += "[^/]*";
199
+ }
200
+ } else if (c === "?") {
201
+ re += "[^/]";
202
+ } else if (".+^${}()|[]\\".includes(c)) {
203
+ re += `\\${c}`;
204
+ } else {
205
+ re += c;
206
+ }
207
+ }
208
+ return new RegExp(`^${re}$`);
209
+ }
176
210
  var FileAgent = class {
177
211
  root;
178
212
  constructor(cwd) {
@@ -220,6 +254,70 @@ var FileAgent = class {
220
254
  async createDir(relPath) {
221
255
  await fs.mkdir(this.resolve(relPath), { recursive: true });
222
256
  }
257
+ /** Recursively yield every file path under `dir`, skipping ignored dirs. */
258
+ async *walkFiles(dir) {
259
+ let entries;
260
+ try {
261
+ entries = await fs.readdir(dir, { withFileTypes: true });
262
+ } catch {
263
+ return;
264
+ }
265
+ for (const e of entries) {
266
+ if (e.isDirectory()) {
267
+ if (IGNORE_DIRS.has(e.name))
268
+ continue;
269
+ yield* this.walkFiles(path.join(dir, e.name));
270
+ } else {
271
+ yield path.join(dir, e.name);
272
+ }
273
+ }
274
+ }
275
+ rel(abs) {
276
+ return path.relative(this.root, abs).split(path.sep).join("/");
277
+ }
278
+ /** Search file contents (regex). Returns `path:line: text` matches. */
279
+ async grep(pattern, maxResults = 80) {
280
+ let re;
281
+ try {
282
+ re = new RegExp(pattern);
283
+ } catch {
284
+ re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
285
+ }
286
+ const out = [];
287
+ for await (const abs of this.walkFiles(this.root)) {
288
+ if (out.length >= maxResults)
289
+ break;
290
+ let content;
291
+ try {
292
+ content = await fs.readFile(abs, "utf8");
293
+ } catch {
294
+ continue;
295
+ }
296
+ const lines = content.split("\n");
297
+ for (let i = 0; i < lines.length; i++) {
298
+ if (re.test(lines[i])) {
299
+ out.push(`${this.rel(abs)}:${i + 1}: ${lines[i].trim().slice(0, 160)}`);
300
+ if (out.length >= maxResults)
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ return out;
306
+ }
307
+ /** Find files whose relative path matches a glob pattern. */
308
+ async glob(pattern, maxResults = 200) {
309
+ const re = globToRegExp(pattern);
310
+ const out = [];
311
+ for await (const abs of this.walkFiles(this.root)) {
312
+ const rel = this.rel(abs);
313
+ if (re.test(rel)) {
314
+ out.push(rel);
315
+ if (out.length >= maxResults)
316
+ break;
317
+ }
318
+ }
319
+ return out.sort((a, b) => a.localeCompare(b));
320
+ }
223
321
  };
224
322
 
225
323
  // ../core/dist/system-prompt.js
@@ -227,10 +325,14 @@ var SYSTEM_PROMPT = `You are StackAI, an autonomous coding agent operating insid
227
325
 
228
326
  You can read, write, and edit files by calling the provided tools. Be DECISIVE and make the FEWEST tool calls possible \u2014 every call is a slow round-trip.
229
327
 
328
+ Tools: read_file, write_file, edit_file, list_files, create_dir, search_files (regex content search), find_files (glob).
329
+
230
330
  How to work:
231
- 1. To create a new file: call write_file once. Do NOT list_files or read_file first.
232
- 2. To change an existing file: call read_file ONCE to see its contents, then make ONE edit_file (or one write_file) call to apply the change.
233
- 3. When the task is done, stop calling tools and reply with ONE short sentence summarizing what you changed.
331
+ 1. To explore an unfamiliar project, use find_files (e.g. "src/**/*.ts") and search_files (regex) instead of reading everything.
332
+ 2. To create a new file: call write_file once. Do NOT list_files or read_file first.
333
+ 3. To change an existing file: call read_file ONCE to see its contents, then make ONE edit_file (or one write_file) call to apply the change.
334
+ 4. If project instructions were provided (from STACKAI.md / AGENTS.md), follow them.
335
+ 5. When the task is done, stop calling tools and reply with ONE short sentence summarizing what you changed.
234
336
 
235
337
  Hard rules:
236
338
  - Make each distinct tool call AT MOST ONCE. Never repeat the same read or edit.
@@ -307,6 +409,30 @@ var TOOLS = [
307
409
  required: ["path"]
308
410
  }
309
411
  }
412
+ },
413
+ {
414
+ type: "function",
415
+ function: {
416
+ name: "search_files",
417
+ description: "Search file contents across the project with a regular expression. Returns matching `path:line: text`.",
418
+ parameters: {
419
+ type: "object",
420
+ properties: { pattern: { type: "string" } },
421
+ required: ["pattern"]
422
+ }
423
+ }
424
+ },
425
+ {
426
+ type: "function",
427
+ function: {
428
+ name: "find_files",
429
+ description: "Find files by glob pattern (e.g. `src/**/*.ts`, `*.json`). Returns matching paths.",
430
+ parameters: {
431
+ type: "object",
432
+ properties: { pattern: { type: "string" } },
433
+ required: ["pattern"]
434
+ }
435
+ }
310
436
  }
311
437
  ];
312
438
  var AgentRunner = class {
@@ -318,16 +444,40 @@ var AgentRunner = class {
318
444
  }
319
445
  async run({ prompt, cwd, onStep }) {
320
446
  const files = new FileAgent(cwd);
321
- const messages = [
322
- { role: "system", content: SYSTEM_PROMPT },
323
- { role: "user", content: prompt }
324
- ];
447
+ const messages = await this.systemMessages(files);
448
+ messages.push({ role: "user", content: prompt });
325
449
  return this.runLoop(messages, files, onStep);
326
450
  }
327
451
  /** Start a stateful chat session that retains history across prompts. */
328
452
  session(cwd) {
329
453
  return new AgentSession(this, cwd);
330
454
  }
455
+ /**
456
+ * Build the leading system messages: the base system prompt plus any
457
+ * project context found in STACKAI.md / AGENTS.md at the project root.
458
+ * @internal
459
+ */
460
+ async systemMessages(files) {
461
+ const messages = [
462
+ { role: "system", content: SYSTEM_PROMPT }
463
+ ];
464
+ for (const name of ["STACKAI.md", "AGENTS.md"]) {
465
+ try {
466
+ const ctx = (await files.readFile(name)).trim();
467
+ if (ctx) {
468
+ messages.push({
469
+ role: "system",
470
+ content: `Project instructions from ${name}:
471
+
472
+ ${ctx}`
473
+ });
474
+ break;
475
+ }
476
+ } catch {
477
+ }
478
+ }
479
+ return messages;
480
+ }
331
481
  /**
332
482
  * Run the tool-call loop over an existing message history (mutated in place).
333
483
  * Shared by one-shot run() and the interactive AgentSession.
@@ -409,6 +559,14 @@ var AgentRunner = class {
409
559
  case "create_dir":
410
560
  await files.createDir(str(args.path));
411
561
  return `Created ${str(args.path)}`;
562
+ case "search_files": {
563
+ const matches = await files.grep(str(args.pattern));
564
+ return matches.join("\n") || "(no matches)";
565
+ }
566
+ case "find_files": {
567
+ const found = await files.glob(str(args.pattern));
568
+ return found.join("\n") || "(no files matched)";
569
+ }
412
570
  default:
413
571
  return `Error: unknown tool ${name}`;
414
572
  }
@@ -422,16 +580,19 @@ function str(value) {
422
580
  }
423
581
  var AgentSession = class {
424
582
  runner;
425
- messages = [
426
- { role: "system", content: SYSTEM_PROMPT }
427
- ];
583
+ messages = [];
428
584
  files;
585
+ initialized = false;
429
586
  constructor(runner, cwd) {
430
587
  this.runner = runner;
431
588
  this.files = new FileAgent(cwd);
432
589
  }
433
590
  /** Send a user message; the agent acts with full prior context. */
434
591
  async send(prompt, onStep) {
592
+ if (!this.initialized) {
593
+ this.messages = await this.runner.systemMessages(this.files);
594
+ this.initialized = true;
595
+ }
435
596
  this.messages.push({ role: "user", content: prompt });
436
597
  return this.runner.runLoop(this.messages, this.files, onStep);
437
598
  }
@@ -501,36 +662,99 @@ import { Box as Box2, Text as Text2, useApp } from "ink";
501
662
  import Spinner from "ink-spinner";
502
663
 
503
664
  // src/ui/format.ts
504
- function describe(step) {
505
- const p = step.args.path ?? step.args.dir ?? "";
665
+ var ACCENT = "#e8ff47";
666
+ var MAX_PREVIEW = 6;
667
+ function splitLines(s) {
668
+ return s.replace(/\n+$/, "").split("\n");
669
+ }
670
+ function clip(s, n = 76) {
671
+ return s.length > n ? `${s.slice(0, n)}\u2026` : s;
672
+ }
673
+ function preview(lines, prefix, color) {
674
+ const out = lines.slice(0, MAX_PREVIEW).map((l) => ({ kind: "sub", color, text: `${prefix} ${clip(l)}` }));
675
+ if (lines.length > MAX_PREVIEW) {
676
+ out.push({
677
+ kind: "sub",
678
+ color: "gray",
679
+ text: `\u2026 ${lines.length - MAX_PREVIEW} more lines`
680
+ });
681
+ }
682
+ return out;
683
+ }
684
+ function toolEntries(step) {
685
+ const args = step.args;
686
+ const path3 = String(args.path ?? args.dir ?? "");
506
687
  switch (step.name) {
688
+ case "write_file": {
689
+ const lines = splitLines(String(args.content ?? ""));
690
+ return [
691
+ { kind: "tool", color: ACCENT, label: `Write(${path3})` },
692
+ { kind: "sub", color: "green", text: `+${lines.length} lines` },
693
+ ...preview(lines, "+", "green")
694
+ ];
695
+ }
696
+ case "edit_file": {
697
+ const removed = splitLines(String(args.oldStr ?? ""));
698
+ const added = splitLines(String(args.newStr ?? ""));
699
+ return [
700
+ { kind: "tool", color: ACCENT, label: `Edit(${path3})` },
701
+ {
702
+ kind: "sub",
703
+ color: "gray",
704
+ text: `+${added.length} -${removed.length} lines`
705
+ },
706
+ ...preview(removed, "-", "red"),
707
+ ...preview(added, "+", "green")
708
+ ];
709
+ }
507
710
  case "read_file":
508
- return `Reading ${p}`;
509
- case "write_file":
510
- return `Writing ${p}`;
511
- case "edit_file":
512
- return `Editing ${p}`;
711
+ return [{ kind: "tool", color: ACCENT, label: `Read(${path3})` }];
513
712
  case "list_files":
514
- return `Listing ${p || "."}`;
713
+ return [{ kind: "tool", color: ACCENT, label: `List(${path3 || "."})` }];
515
714
  case "create_dir":
516
- return `Creating ${p}/`;
715
+ return [{ kind: "tool", color: ACCENT, label: `Create(${path3}/)` }];
716
+ case "search_files":
717
+ return [
718
+ { kind: "tool", color: ACCENT, label: `Search(${String(args.pattern ?? "")})` }
719
+ ];
720
+ case "find_files":
721
+ return [
722
+ { kind: "tool", color: ACCENT, label: `Find(${String(args.pattern ?? "")})` }
723
+ ];
517
724
  default:
518
- return step.name;
725
+ return [{ kind: "tool", color: ACCENT, label: step.name }];
519
726
  }
520
727
  }
521
728
  function applyStep(entries, step) {
522
729
  if (step.type === "token") {
523
730
  const last = entries[entries.length - 1];
524
731
  if (last && last.kind === "text") {
525
- return [...entries.slice(0, -1), { kind: "text", text: last.text + step.text }];
732
+ return [
733
+ ...entries.slice(0, -1),
734
+ { kind: "text", text: last.text + step.text }
735
+ ];
526
736
  }
527
737
  return [...entries, { kind: "text", text: step.text }];
528
738
  }
529
739
  if (step.type === "tool_call") {
530
- return [...entries, { kind: "tool", color: "gray", text: describe(step) }];
740
+ return [...entries, ...toolEntries(step)];
531
741
  }
532
- if (step.type === "tool_result" && !step.ok) {
533
- return [...entries, { kind: "tool", color: "red", text: step.detail }];
742
+ if (step.type === "tool_result") {
743
+ if (!step.ok) {
744
+ return [
745
+ ...entries,
746
+ { kind: "sub", color: "red", text: `\u2717 ${step.detail}` }
747
+ ];
748
+ }
749
+ if (step.name === "read_file") {
750
+ const n = step.detail ? splitLines(step.detail).length : 0;
751
+ return [...entries, { kind: "sub", color: "gray", text: `read ${n} lines` }];
752
+ }
753
+ if (step.name === "search_files" || step.name === "find_files") {
754
+ const empty = /^\(no /.test(step.detail);
755
+ const n = empty ? 0 : splitLines(step.detail).length;
756
+ return [...entries, { kind: "sub", color: "gray", text: `${n} results` }];
757
+ }
534
758
  }
535
759
  return entries;
536
760
  }
@@ -539,12 +763,18 @@ function applyStep(entries, step) {
539
763
  import { Box, Text } from "ink";
540
764
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
541
765
  function EntryLines({ entries }) {
542
- return /* @__PURE__ */ jsx(Fragment, { children: entries.map(
543
- (e, i) => e.kind === "tool" ? /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: e.color, children: [
544
- "\u2192 ",
545
- e.text
546
- ] }) }, i) : /* @__PURE__ */ jsx(Box, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { children: e.text }) }, i)
547
- ) });
766
+ return /* @__PURE__ */ jsx(Fragment, { children: entries.map((e, i) => {
767
+ if (e.kind === "tool") {
768
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: e.color, bold: true, children: [
769
+ "\u25CF ",
770
+ e.label
771
+ ] }) }, i);
772
+ }
773
+ if (e.kind === "sub") {
774
+ return /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: e.color, children: e.text }) }, i);
775
+ }
776
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { children: e.text }) }, i);
777
+ }) });
548
778
  }
549
779
 
550
780
  // src/ui/run-view.tsx
@@ -597,7 +827,7 @@ import { Box as Box3, Text as Text3, useApp as useApp2 } from "ink";
597
827
  import Spinner2 from "ink-spinner";
598
828
  import TextInput from "ink-text-input";
599
829
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
600
- var ACCENT = "#e8ff47";
830
+ var ACCENT2 = "#e8ff47";
601
831
  var LOGO = [
602
832
  "\u2588\u2580\u2580 \u2580\u2588\u2580 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2584\u2580 \u2588\u2580\u2588 \u2588",
603
833
  "\u2584\u2584\u2588 \u2588 \u2588\u2580\u2588 \u2588\u2584\u2584 \u2588\u2580\u2584 \u2588\u2580\u2588 \u2588"
@@ -647,11 +877,11 @@ function Interactive({ session, cwd, version }) {
647
877
  {
648
878
  flexDirection: "column",
649
879
  borderStyle: "round",
650
- borderColor: ACCENT,
880
+ borderColor: ACCENT2,
651
881
  paddingX: 2,
652
882
  paddingY: 1,
653
883
  children: [
654
- LOGO.map((line, i) => /* @__PURE__ */ jsx3(Text3, { color: ACCENT, bold: true, children: line }, i)),
884
+ LOGO.map((line, i) => /* @__PURE__ */ jsx3(Text3, { color: ACCENT2, bold: true, children: line }, i)),
655
885
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
656
886
  "v",
657
887
  version,
@@ -664,12 +894,12 @@ function Interactive({ session, cwd, version }) {
664
894
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
665
895
  "Describe what you want to build \xB7 type",
666
896
  " ",
667
- /* @__PURE__ */ jsx3(Text3, { color: ACCENT, children: "/exit" }),
897
+ /* @__PURE__ */ jsx3(Text3, { color: ACCENT2, children: "/exit" }),
668
898
  " to quit"
669
899
  ] }) }),
670
900
  history.map(
671
901
  (block, i) => block.kind === "user" ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
672
- /* @__PURE__ */ jsxs3(Text3, { color: ACCENT, bold: true, children: [
902
+ /* @__PURE__ */ jsxs3(Text3, { color: ACCENT2, bold: true, children: [
673
903
  "\u203A",
674
904
  " "
675
905
  ] }),
@@ -685,13 +915,13 @@ function Interactive({ session, cwd, version }) {
685
915
  {
686
916
  marginTop: 1,
687
917
  borderStyle: "round",
688
- borderColor: busy ? "gray" : ACCENT,
918
+ borderColor: busy ? "gray" : ACCENT2,
689
919
  paddingX: 1,
690
920
  children: busy ? /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
691
921
  /* @__PURE__ */ jsx3(Spinner2, { type: "dots" }),
692
922
  " working\u2026"
693
923
  ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
694
- /* @__PURE__ */ jsx3(Text3, { color: ACCENT, children: "\u203A " }),
924
+ /* @__PURE__ */ jsx3(Text3, { color: ACCENT2, children: "\u203A " }),
695
925
  /* @__PURE__ */ jsx3(
696
926
  TextInput,
697
927
  {
@@ -776,7 +1006,7 @@ function LoginView() {
776
1006
 
777
1007
  // src/cli.tsx
778
1008
  import { jsx as jsx5 } from "react/jsx-runtime";
779
- var VERSION = "0.1.2";
1009
+ var VERSION = "0.1.4";
780
1010
  var HELP = `
781
1011
  StackAI \u2014 AI coding agent in your terminal
782
1012
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackai",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "StackAI — AI coding agent in your terminal. Read, write, and edit code with AI.",
5
5
  "type": "module",
6
6
  "bin": {