@hizliemre/horse-code 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import {
2
+ createWebFetchTool,
3
+ readOnlyRegistry
4
+ } from "./chunk-C3AOCIS2.js";
5
+ import "./chunk-ZSQ24YDJ.js";
6
+ import "./chunk-MZM24M5M.js";
7
+ import {
8
+ defaultGitRunner
9
+ } from "./chunk-LPQU436C.js";
10
+ import "./chunk-LLJWUHI6.js";
11
+ import "./chunk-PTOFSTLL.js";
12
+ import "./chunk-63E73TGI.js";
13
+ import "./chunk-6S4WWQMN.js";
14
+ import {
15
+ runToCompletion
16
+ } from "./chunk-FNUGG2FG.js";
17
+ import "./chunk-ZPJP2VH5.js";
18
+ import "./chunk-4M6LXNG2.js";
19
+ import "./chunk-6W4UH2BQ.js";
20
+
21
+ // src/engine/research.ts
22
+ import { mkdir, writeFile } from "fs/promises";
23
+ import { dirname, join, relative } from "path";
24
+ import { existsSync } from "fs";
25
+ var RESEARCH_MAX_TURNS = 120;
26
+ function researchDir(trackedFiles2) {
27
+ const known = [/^docs\/research\//i, /^docs\/adr\//i, /^docs\/decisions\//i, /^research\//i, /^\.planning\//i];
28
+ for (const re of known) {
29
+ const hit = trackedFiles2.find((f) => re.test(f.replace(/\\/g, "/")));
30
+ if (hit) return dirname(hit.replace(/\\/g, "/"));
31
+ }
32
+ return "docs/research";
33
+ }
34
+ function reportName(title, now = /* @__PURE__ */ new Date()) {
35
+ const slug = (title || "research").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
36
+ const day = now.toISOString().slice(0, 10);
37
+ return `${day}-${slug || "research"}.md`;
38
+ }
39
+ var researchRule = "You are researching a question and writing ONE document. You are not changing this project: you have no tool that writes a file, and the report is your final answer \u2014 horse-code writes it down.\n\nGround the answer in THIS repository before anything else. Read the code that the question is about; a recommendation that ignores what is already here is a recommendation for a different project.\n\nThe document must contain, in this order:\n1. The question, restated in one or two sentences as you understood it.\n2. What this project does today in the area concerned, with file paths \u2014 the starting position.\n3. The options, one section each. For every option: how it would work here, what it costs, and what it rules out. An option nobody would pick is still worth a paragraph saying why not.\n4. A comparison table across the dimensions that actually decide this one. Choose the dimensions from the question rather than from habit.\n5. A recommendation, with the condition that would change it. A recommendation that nothing could overturn is a preference wearing a suit.\n6. What you could not establish, and what it would take to settle. Say it plainly \u2014 an unexamined assumption presented as a finding is the one failure this document cannot recover from.\n\nWrite markdown. Cite file paths for claims about this project and URLs for claims about the world. Where you are reasoning from your own knowledge rather than from something you read, say so.";
40
+ async function runResearch(opts) {
41
+ const { deps, workdir } = opts;
42
+ const dir = researchDir(opts.trackedFiles ?? await trackedFiles(workdir));
43
+ const rel = join(dir, reportName(opts.title));
44
+ const tools = readOnlyRegistry(deps, { remember: true });
45
+ tools.register(createWebFetchTool());
46
+ const language = opts.language ? `
47
+
48
+ Write the document in ${opts.language}. Keep file paths, identifiers and URLs exactly as they are.` : "";
49
+ const rules = deps.roleRegistry.ruleSuffix();
50
+ const last = await runToCompletion({
51
+ ...deps.roleRegistry.fallbackOpts("analyst"),
52
+ systemPrompt: researchRule + language + rules,
53
+ messages: [{ role: "user", content: opts.prompt }],
54
+ tools,
55
+ maxTurns: RESEARCH_MAX_TURNS,
56
+ provider: deps.provider,
57
+ permission: deps.permission,
58
+ approve: deps.approve,
59
+ cwd: workdir,
60
+ signal: deps.signal,
61
+ onActivity: deps.onActivity,
62
+ onLiveActivity: deps.onLiveActivity,
63
+ inbox: deps.inbox,
64
+ ...opts.note ? { onSay: opts.note } : {}
65
+ });
66
+ const body = (last.content ?? "").trim();
67
+ if (!body) return { reportPath: rel, written: false, committed: false };
68
+ const abs = join(workdir, rel);
69
+ await mkdir(dirname(abs), { recursive: true });
70
+ await writeFile(abs, `${body}
71
+ `, "utf8");
72
+ return { reportPath: rel, written: true, committed: await commitReport(workdir, rel, opts.title) };
73
+ }
74
+ async function trackedFiles(cwd) {
75
+ try {
76
+ const r = await defaultGitRunner(["ls-files"], cwd);
77
+ return r.code === 0 ? r.stdout.split("\n").filter(Boolean) : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+ async function commitReport(workdir, rel, title) {
83
+ if (!existsSync(join(workdir, ".git")) && !existsSync(join(workdir, rel))) return false;
84
+ const add = await defaultGitRunner(["add", "--", rel], workdir);
85
+ if (add.code !== 0) return false;
86
+ const staged = await defaultGitRunner(["diff", "--cached", "--quiet", "--", rel], workdir);
87
+ if (staged.code === 0) return false;
88
+ const r = await defaultGitRunner(["commit", "-m", `docs(research): ${title || "findings"}`, "--", rel], workdir);
89
+ return r.code === 0;
90
+ }
91
+ function describeResearch(r, branch) {
92
+ if (!r.written) return "The research produced no document \u2014 nothing was written.";
93
+ const where = r.committed ? `Committed on \`${branch}\`.` : "Written, but not committed \u2014 it is in the working tree.";
94
+ return `\u{1F4C4} **Research report:** \`${r.reportPath}\`
95
+ ${where}
96
+
97
+ _No code was changed: the lane runs without a write tool at all._`;
98
+ }
99
+ function plannedPath(title, trackedFiles2) {
100
+ return relative(".", join(researchDir(trackedFiles2), reportName(title))).replace(/\\/g, "/");
101
+ }
102
+ export {
103
+ RESEARCH_MAX_TURNS,
104
+ commitReport,
105
+ describeResearch,
106
+ plannedPath,
107
+ reportName,
108
+ researchDir,
109
+ researchRule,
110
+ runResearch
111
+ };
@@ -3,9 +3,9 @@ import {
3
3
  saveMcpServer,
4
4
  saveRoleSkills,
5
5
  saveSkillSource
6
- } from "./chunk-JR2JLRE3.js";
6
+ } from "./chunk-KFB5H4HX.js";
7
7
  import "./chunk-H2FDGPVW.js";
8
- import "./chunk-G45RWL7S.js";
8
+ import "./chunk-PEAZQPEZ.js";
9
9
  export {
10
10
  removeSkillSource,
11
11
  saveMcpServer,
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ColdStartGate,
2
3
  SpentSources,
3
4
  TRACE_CONCURRENCY,
4
5
  buildBrief,
@@ -7,15 +8,17 @@ import {
7
8
  planFor,
8
9
  runTraces,
9
10
  traceableFiles
10
- } from "./chunk-YULQ4URQ.js";
11
- import "./chunk-G45RWL7S.js";
11
+ } from "./chunk-UKIIQD6Q.js";
12
+ import "./chunk-MZM24M5M.js";
13
+ import "./chunk-PEAZQPEZ.js";
12
14
  import "./chunk-LPQU436C.js";
13
15
  import "./chunk-6S4WWQMN.js";
14
- import "./chunk-JLWQCA7B.js";
16
+ import "./chunk-FNUGG2FG.js";
15
17
  import "./chunk-ZPJP2VH5.js";
16
18
  import "./chunk-4M6LXNG2.js";
17
19
  import "./chunk-6W4UH2BQ.js";
18
20
  export {
21
+ ColdStartGate,
19
22
  SpentSources,
20
23
  TRACE_CONCURRENCY,
21
24
  buildBrief,
@@ -8,12 +8,12 @@ import {
8
8
  describeSizeDoubt,
9
9
  sizeRequest,
10
10
  triageFinding
11
- } from "./chunk-372X5HHU.js";
12
- import "./chunk-BFIZMM4G.js";
11
+ } from "./chunk-J6P3CXUR.js";
12
+ import "./chunk-LLJWUHI6.js";
13
+ import "./chunk-PTOFSTLL.js";
13
14
  import "./chunk-63E73TGI.js";
14
- import "./chunk-CYLPQWIF.js";
15
15
  import "./chunk-6S4WWQMN.js";
16
- import "./chunk-JLWQCA7B.js";
16
+ import "./chunk-FNUGG2FG.js";
17
17
  import "./chunk-ZPJP2VH5.js";
18
18
  import "./chunk-4M6LXNG2.js";
19
19
  import "./chunk-6W4UH2BQ.js";
@@ -3,41 +3,44 @@ import {
3
3
  describeFix,
4
4
  dirtyPaths,
5
5
  runFix
6
- } from "./chunk-3ACDNDCG.js";
6
+ } from "./chunk-ST6LIEPZ.js";
7
7
  import {
8
8
  respondIn
9
9
  } from "./chunk-M2RKCIGV.js";
10
+ import {
11
+ askInUserLanguage,
12
+ inUserLanguage
13
+ } from "./chunk-DCK2QQ2S.js";
10
14
  import {
11
15
  buildAskUserTool,
16
+ editFileTool,
17
+ refreshAfterChange,
18
+ shellTool,
19
+ writeFileTool
20
+ } from "./chunk-M2RP2AGC.js";
21
+ import {
12
22
  buildRememberTool,
13
23
  constitutionNote,
14
24
  createWebFetchTool,
15
25
  deferMcp,
16
- editFileTool,
17
26
  featureSlugFor,
18
27
  gitTool,
19
- refreshAfterChange,
20
- shellTool,
21
28
  specsDir,
22
- verifyPaths,
23
- writeFileTool
24
- } from "./chunk-27F44PBD.js";
25
- import {
26
- askInUserLanguage,
27
- inUserLanguage
28
- } from "./chunk-UTHLEW5V.js";
29
+ verifyPaths
30
+ } from "./chunk-C3AOCIS2.js";
29
31
  import "./chunk-ZSQ24YDJ.js";
32
+ import "./chunk-UKIIQD6Q.js";
30
33
  import {
31
34
  buildSkillTool
32
- } from "./chunk-YULQ4URQ.js";
33
- import "./chunk-G45RWL7S.js";
35
+ } from "./chunk-MZM24M5M.js";
36
+ import "./chunk-PEAZQPEZ.js";
34
37
  import {
35
38
  defaultGitRunner
36
39
  } from "./chunk-LPQU436C.js";
37
40
  import {
38
41
  describeEscalation,
39
42
  triageFinding
40
- } from "./chunk-372X5HHU.js";
43
+ } from "./chunk-J6P3CXUR.js";
41
44
  import {
42
45
  BATCH_TOOLS_NOTE,
43
46
  contextTools,
@@ -47,16 +50,16 @@ import {
47
50
  projectToolsNote,
48
51
  readFileTool,
49
52
  reinforceUsed
50
- } from "./chunk-BFIZMM4G.js";
51
- import "./chunk-63E73TGI.js";
53
+ } from "./chunk-LLJWUHI6.js";
52
54
  import {
53
55
  ToolRegistry
54
- } from "./chunk-CYLPQWIF.js";
56
+ } from "./chunk-PTOFSTLL.js";
57
+ import "./chunk-63E73TGI.js";
55
58
  import "./chunk-6S4WWQMN.js";
56
59
  import {
57
60
  handedOver,
58
61
  runToCompletion
59
- } from "./chunk-JLWQCA7B.js";
62
+ } from "./chunk-FNUGG2FG.js";
60
63
  import "./chunk-ZPJP2VH5.js";
61
64
  import {
62
65
  loadGraphSync
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hizliemre/horse-code",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Terminal coding agent: one sentence to reviewed, committed code — in its own git worktree",
5
5
  "license": "MIT",
6
6
  "author": "Emre Hızlı <hizliemre@gmail.com>",
@@ -2,86 +2,6 @@ import {
2
2
  sessionBase
3
3
  } from "./chunk-6W4UH2BQ.js";
4
4
 
5
- // src/tui/format.ts
6
- function fmtTokens(n) {
7
- if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
8
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
9
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
10
- return String(n);
11
- }
12
- function fmtDuration(ms) {
13
- const s = Math.max(0, Math.round(ms / 1e3));
14
- if (s < 60) return `${s}s`;
15
- return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`;
16
- }
17
- function relTime(then, now = Date.now()) {
18
- const s = Math.max(0, Math.round((now - then) / 1e3));
19
- if (s < 60) return "just now";
20
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
21
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
22
- return `${Math.floor(s / 86400)}d ago`;
23
- }
24
- function stripThinking(text) {
25
- let out = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
26
- const lastClose = out.toLowerCase().lastIndexOf("</think>");
27
- if (lastClose !== -1 && !/<think>/i.test(out.slice(0, lastClose))) {
28
- out = out.slice(lastClose + "</think>".length);
29
- }
30
- const open = out.toLowerCase().lastIndexOf("<think>");
31
- if (open !== -1) out = out.slice(0, open).trimEnd();
32
- return out.trimStart();
33
- }
34
-
35
- // src/agent/attach.ts
36
- import { readFileSync, statSync } from "fs";
37
- import { isAbsolute, resolve } from "path";
38
- import { homedir } from "os";
39
- var IMAGE_EXT = {
40
- png: "image/png",
41
- jpg: "image/jpeg",
42
- jpeg: "image/jpeg",
43
- gif: "image/gif",
44
- webp: "image/webp"
45
- };
46
- var MAX_IMAGE_BYTES = 5e6;
47
- var CANDIDATE = /(?:"([^"\n]+?\.(?:png|jpe?g|gif|webp))"|'([^'\n]+?\.(?:png|jpe?g|gif|webp))'|((?:~|\.{0,2}\/|[A-Za-z0-9_.-]+\/)[^\n"'`]*?\.(?:png|jpe?g|gif|webp)))/gi;
48
- function expand(p, cwd) {
49
- const t = p.trim();
50
- if (t.startsWith("~/")) return resolve(homedir(), t.slice(2));
51
- return isAbsolute(t) ? t : resolve(cwd, t);
52
- }
53
- function attachedImages(text, cwd) {
54
- const out = [];
55
- const seen = /* @__PURE__ */ new Set();
56
- for (const m of text.matchAll(CANDIDATE)) {
57
- const raw = m[1] ?? m[2] ?? m[3];
58
- if (!raw) continue;
59
- if (/^[a-z]+:\/\//i.test(raw)) continue;
60
- const abs = expand(raw, cwd);
61
- if (seen.has(abs)) continue;
62
- const ext = abs.slice(abs.lastIndexOf(".") + 1).toLowerCase();
63
- const mime = IMAGE_EXT[ext];
64
- if (!mime) continue;
65
- try {
66
- const st = statSync(abs);
67
- if (!st.isFile() || st.size > MAX_IMAGE_BYTES || st.size === 0) continue;
68
- out.push(`data:${mime};base64,${readFileSync(abs).toString("base64")}`);
69
- seen.add(abs);
70
- } catch {
71
- }
72
- }
73
- return out;
74
- }
75
- var OWN_PASTE = /\S*[\\/]\.horsecode[\\/]pastes[\\/][^\s"'`)\]]+\.(?:png|jpe?g|gif|webp)/gi;
76
- var HANDED_OVER = "[screenshot pasted by the developer]";
77
- function withoutPastePaths(text) {
78
- return text.replace(OWN_PASTE, HANDED_OVER);
79
- }
80
- function handedOver(text, cwd) {
81
- const images = attachedImages(text, cwd);
82
- return { content: withoutPastePaths(text), ...images.length ? { images } : {} };
83
- }
84
-
85
5
  // src/obs/telemetry.ts
86
6
  import { AsyncLocalStorage } from "async_hooks";
87
7
  import { randomBytes } from "crypto";
@@ -227,6 +147,86 @@ function telemetry() {
227
147
  return active;
228
148
  }
229
149
 
150
+ // src/tui/format.ts
151
+ function fmtTokens(n) {
152
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
153
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
154
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
155
+ return String(n);
156
+ }
157
+ function fmtDuration(ms) {
158
+ const s = Math.max(0, Math.round(ms / 1e3));
159
+ if (s < 60) return `${s}s`;
160
+ return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`;
161
+ }
162
+ function relTime(then, now = Date.now()) {
163
+ const s = Math.max(0, Math.round((now - then) / 1e3));
164
+ if (s < 60) return "just now";
165
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
166
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
167
+ return `${Math.floor(s / 86400)}d ago`;
168
+ }
169
+ function stripThinking(text) {
170
+ let out = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
171
+ const lastClose = out.toLowerCase().lastIndexOf("</think>");
172
+ if (lastClose !== -1 && !/<think>/i.test(out.slice(0, lastClose))) {
173
+ out = out.slice(lastClose + "</think>".length);
174
+ }
175
+ const open = out.toLowerCase().lastIndexOf("<think>");
176
+ if (open !== -1) out = out.slice(0, open).trimEnd();
177
+ return out.trimStart();
178
+ }
179
+
180
+ // src/agent/attach.ts
181
+ import { readFileSync, statSync } from "fs";
182
+ import { isAbsolute, resolve } from "path";
183
+ import { homedir } from "os";
184
+ var IMAGE_EXT = {
185
+ png: "image/png",
186
+ jpg: "image/jpeg",
187
+ jpeg: "image/jpeg",
188
+ gif: "image/gif",
189
+ webp: "image/webp"
190
+ };
191
+ var MAX_IMAGE_BYTES = 5e6;
192
+ var CANDIDATE = /(?:"([^"\n]+?\.(?:png|jpe?g|gif|webp))"|'([^'\n]+?\.(?:png|jpe?g|gif|webp))'|((?:~|\.{0,2}\/|[A-Za-z0-9_.-]+\/)[^\n"'`]*?\.(?:png|jpe?g|gif|webp)))/gi;
193
+ function expand(p, cwd) {
194
+ const t = p.trim();
195
+ if (t.startsWith("~/")) return resolve(homedir(), t.slice(2));
196
+ return isAbsolute(t) ? t : resolve(cwd, t);
197
+ }
198
+ function attachedImages(text, cwd) {
199
+ const out = [];
200
+ const seen = /* @__PURE__ */ new Set();
201
+ for (const m of text.matchAll(CANDIDATE)) {
202
+ const raw = m[1] ?? m[2] ?? m[3];
203
+ if (!raw) continue;
204
+ if (/^[a-z]+:\/\//i.test(raw)) continue;
205
+ const abs = expand(raw, cwd);
206
+ if (seen.has(abs)) continue;
207
+ const ext = abs.slice(abs.lastIndexOf(".") + 1).toLowerCase();
208
+ const mime = IMAGE_EXT[ext];
209
+ if (!mime) continue;
210
+ try {
211
+ const st = statSync(abs);
212
+ if (!st.isFile() || st.size > MAX_IMAGE_BYTES || st.size === 0) continue;
213
+ out.push(`data:${mime};base64,${readFileSync(abs).toString("base64")}`);
214
+ seen.add(abs);
215
+ } catch {
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+ var OWN_PASTE = /\S*[\\/]\.horsecode[\\/]pastes[\\/][^\s"'`)\]]+\.(?:png|jpe?g|gif|webp)/gi;
221
+ var HANDED_OVER = "[screenshot pasted by the developer]";
222
+ function withoutPastePaths(text) {
223
+ return text.replace(OWN_PASTE, HANDED_OVER);
224
+ }
225
+ function handedOver(text, cwd) {
226
+ const images = attachedImages(text, cwd);
227
+ return { content: withoutPastePaths(text), ...images.length ? { images } : {} };
228
+ }
229
+
230
230
  // src/agent/recall.ts
231
231
  var RECALLABLE = /* @__PURE__ */ new Set(["read_file", "grep", "glob", "graph_trace", "graph_find", "graph_context"]);
232
232
  var INVALIDATING = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
@@ -1161,12 +1161,6 @@ async function runToCompletion(opts) {
1161
1161
  }
1162
1162
 
1163
1163
  export {
1164
- fmtTokens,
1165
- fmtDuration,
1166
- relTime,
1167
- stripThinking,
1168
- handedOver,
1169
- truncateSafe,
1170
1164
  Telemetry,
1171
1165
  sampleMemory,
1172
1166
  estimateFreezeSeconds,
@@ -1174,6 +1168,12 @@ export {
1174
1168
  clearPerfMarks,
1175
1169
  setTelemetry,
1176
1170
  telemetry,
1171
+ fmtTokens,
1172
+ fmtDuration,
1173
+ relTime,
1174
+ stripThinking,
1175
+ handedOver,
1176
+ truncateSafe,
1177
1177
  redactSecrets,
1178
1178
  runRoleAgent,
1179
1179
  runToCompletion