amness 0.0.0-stage → 0.1.2

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -2
  3. package/dist/cli/acpcli.d.ts +7 -0
  4. package/dist/cli/acpcli.js +14 -0
  5. package/dist/cli/amness.d.ts +2 -0
  6. package/dist/cli/amness.js +694 -0
  7. package/dist/cli/benchcli.d.ts +5 -0
  8. package/dist/cli/benchcli.js +160 -0
  9. package/dist/cli/config.d.ts +79 -0
  10. package/dist/cli/config.js +111 -0
  11. package/dist/cli/demo.d.ts +13 -0
  12. package/dist/cli/demo.js +44 -0
  13. package/dist/cli/detect.d.ts +2 -0
  14. package/dist/cli/detect.js +20 -0
  15. package/dist/cli/evalcli.d.ts +15 -0
  16. package/dist/cli/evalcli.js +198 -0
  17. package/dist/cli/format.d.ts +5 -0
  18. package/dist/cli/format.js +55 -0
  19. package/dist/cli/headless.d.ts +2 -0
  20. package/dist/cli/headless.js +169 -0
  21. package/dist/cli/keystore.d.ts +9 -0
  22. package/dist/cli/keystore.js +108 -0
  23. package/dist/cli/model.d.ts +11 -0
  24. package/dist/cli/model.js +19 -0
  25. package/dist/cli/plugincli.d.ts +2 -0
  26. package/dist/cli/plugincli.js +176 -0
  27. package/dist/cli/schedulecli.d.ts +2 -0
  28. package/dist/cli/schedulecli.js +66 -0
  29. package/dist/cli/state.d.ts +11 -0
  30. package/dist/cli/state.js +19 -0
  31. package/dist/cli/tui/ansi.d.ts +38 -0
  32. package/dist/cli/tui/ansi.js +142 -0
  33. package/dist/cli/tui/app.d.ts +328 -0
  34. package/dist/cli/tui/app.js +1358 -0
  35. package/dist/cli/tui/blocks.d.ts +151 -0
  36. package/dist/cli/tui/blocks.js +128 -0
  37. package/dist/cli/tui/brand.d.ts +9 -0
  38. package/dist/cli/tui/brand.js +16 -0
  39. package/dist/cli/tui/canvas.d.ts +92 -0
  40. package/dist/cli/tui/canvas.js +199 -0
  41. package/dist/cli/tui/cards.d.ts +44 -0
  42. package/dist/cli/tui/cards.js +215 -0
  43. package/dist/cli/tui/clipboard.d.ts +5 -0
  44. package/dist/cli/tui/clipboard.js +29 -0
  45. package/dist/cli/tui/commands.d.ts +10 -0
  46. package/dist/cli/tui/commands.js +650 -0
  47. package/dist/cli/tui/complete.d.ts +39 -0
  48. package/dist/cli/tui/complete.js +113 -0
  49. package/dist/cli/tui/custom.d.ts +9 -0
  50. package/dist/cli/tui/custom.js +34 -0
  51. package/dist/cli/tui/diffview.d.ts +25 -0
  52. package/dist/cli/tui/diffview.js +130 -0
  53. package/dist/cli/tui/editor.d.ts +60 -0
  54. package/dist/cli/tui/editor.js +230 -0
  55. package/dist/cli/tui/full.d.ts +86 -0
  56. package/dist/cli/tui/full.js +630 -0
  57. package/dist/cli/tui/keys.d.ts +31 -0
  58. package/dist/cli/tui/keys.js +134 -0
  59. package/dist/cli/tui/markdown.d.ts +37 -0
  60. package/dist/cli/tui/markdown.js +155 -0
  61. package/dist/cli/tui/mentions.d.ts +15 -0
  62. package/dist/cli/tui/mentions.js +48 -0
  63. package/dist/cli/tui/modal.d.ts +26 -0
  64. package/dist/cli/tui/modal.js +107 -0
  65. package/dist/cli/tui/overlays.d.ts +98 -0
  66. package/dist/cli/tui/overlays.js +237 -0
  67. package/dist/cli/tui/pager.d.ts +15 -0
  68. package/dist/cli/tui/pager.js +66 -0
  69. package/dist/cli/tui/pricing.d.ts +9 -0
  70. package/dist/cli/tui/pricing.js +23 -0
  71. package/dist/cli/tui/rich.d.ts +16 -0
  72. package/dist/cli/tui/rich.js +72 -0
  73. package/dist/cli/tui/screen.d.ts +30 -0
  74. package/dist/cli/tui/screen.js +62 -0
  75. package/dist/cli/tui/session.d.ts +77 -0
  76. package/dist/cli/tui/session.js +103 -0
  77. package/dist/cli/tui/sidebar.d.ts +84 -0
  78. package/dist/cli/tui/sidebar.js +114 -0
  79. package/dist/cli/tui/text.d.ts +15 -0
  80. package/dist/cli/tui/text.js +94 -0
  81. package/dist/cli/tui/theme.d.ts +59 -0
  82. package/dist/cli/tui/theme.js +148 -0
  83. package/dist/cli/tui/themes.d.ts +56 -0
  84. package/dist/cli/tui/themes.js +80 -0
  85. package/dist/cli/tui/tokens.d.ts +39 -0
  86. package/dist/cli/tui/tokens.js +71 -0
  87. package/dist/cli/tui/view.d.ts +60 -0
  88. package/dist/cli/tui/view.js +178 -0
  89. package/dist/cli/ui.d.ts +34 -0
  90. package/dist/cli/ui.js +224 -0
  91. package/dist/src/acp.d.ts +28 -0
  92. package/dist/src/acp.js +111 -0
  93. package/dist/src/agents.d.ts +23 -0
  94. package/dist/src/agents.js +71 -0
  95. package/dist/src/anthropic.d.ts +30 -0
  96. package/dist/src/anthropic.js +91 -0
  97. package/dist/src/approver.d.ts +15 -0
  98. package/dist/src/approver.js +40 -0
  99. package/dist/src/bench.d.ts +67 -0
  100. package/dist/src/bench.js +201 -0
  101. package/dist/src/bundle.d.ts +36 -0
  102. package/dist/src/bundle.js +55 -0
  103. package/dist/src/chaos.d.ts +15 -0
  104. package/dist/src/chaos.js +26 -0
  105. package/dist/src/code.d.ts +187 -0
  106. package/dist/src/code.js +590 -0
  107. package/dist/src/codeindex.d.ts +65 -0
  108. package/dist/src/codeindex.js +300 -0
  109. package/dist/src/deep.d.ts +62 -0
  110. package/dist/src/deep.js +177 -0
  111. package/dist/src/diff.d.ts +8 -0
  112. package/dist/src/diff.js +74 -0
  113. package/dist/src/dynamic.d.ts +42 -0
  114. package/dist/src/dynamic.js +133 -0
  115. package/dist/src/embed.d.ts +8 -0
  116. package/dist/src/embed.js +25 -0
  117. package/dist/src/evalcases.d.ts +11 -0
  118. package/dist/src/evalcases.js +196 -0
  119. package/dist/src/evals.d.ts +197 -0
  120. package/dist/src/evals.js +340 -0
  121. package/dist/src/gitflow.d.ts +27 -0
  122. package/dist/src/gitflow.js +56 -0
  123. package/dist/src/grade.d.ts +42 -0
  124. package/dist/src/grade.js +96 -0
  125. package/dist/src/guidance.d.ts +15 -0
  126. package/dist/src/guidance.js +68 -0
  127. package/dist/src/hardcases.d.ts +2 -0
  128. package/dist/src/hardcases.js +94 -0
  129. package/dist/src/harness.d.ts +411 -0
  130. package/dist/src/harness.js +725 -0
  131. package/dist/src/hooks.d.ts +44 -0
  132. package/dist/src/hooks.js +116 -0
  133. package/dist/src/images.d.ts +13 -0
  134. package/dist/src/images.js +27 -0
  135. package/dist/src/index.d.ts +57 -0
  136. package/dist/src/index.js +57 -0
  137. package/dist/src/ledger.d.ts +58 -0
  138. package/dist/src/ledger.js +78 -0
  139. package/dist/src/live.d.ts +16 -0
  140. package/dist/src/live.js +80 -0
  141. package/dist/src/lsp.d.ts +31 -0
  142. package/dist/src/lsp.js +147 -0
  143. package/dist/src/memory.d.ts +128 -0
  144. package/dist/src/memory.js +390 -0
  145. package/dist/src/metrics.d.ts +3 -0
  146. package/dist/src/metrics.js +42 -0
  147. package/dist/src/mining.d.ts +49 -0
  148. package/dist/src/mining.js +126 -0
  149. package/dist/src/openai.d.ts +50 -0
  150. package/dist/src/openai.js +307 -0
  151. package/dist/src/parallel.d.ts +66 -0
  152. package/dist/src/parallel.js +135 -0
  153. package/dist/src/patch.d.ts +45 -0
  154. package/dist/src/patch.js +219 -0
  155. package/dist/src/permissions.d.ts +46 -0
  156. package/dist/src/permissions.js +245 -0
  157. package/dist/src/plugins.d.ts +148 -0
  158. package/dist/src/plugins.js +225 -0
  159. package/dist/src/pluginscan.d.ts +8 -0
  160. package/dist/src/pluginscan.js +20 -0
  161. package/dist/src/procs.d.ts +41 -0
  162. package/dist/src/procs.js +94 -0
  163. package/dist/src/profiles.d.ts +36 -0
  164. package/dist/src/profiles.js +61 -0
  165. package/dist/src/progress.d.ts +29 -0
  166. package/dist/src/progress.js +75 -0
  167. package/dist/src/prompts.d.ts +10 -0
  168. package/dist/src/prompts.js +28 -0
  169. package/dist/src/ratelimit.d.ts +34 -0
  170. package/dist/src/ratelimit.js +116 -0
  171. package/dist/src/replay.d.ts +25 -0
  172. package/dist/src/replay.js +99 -0
  173. package/dist/src/review.d.ts +20 -0
  174. package/dist/src/review.js +49 -0
  175. package/dist/src/route.d.ts +9 -0
  176. package/dist/src/route.js +30 -0
  177. package/dist/src/router.d.ts +61 -0
  178. package/dist/src/router.js +126 -0
  179. package/dist/src/sandbox.d.ts +13 -0
  180. package/dist/src/sandbox.js +68 -0
  181. package/dist/src/schedule.d.ts +20 -0
  182. package/dist/src/schedule.js +59 -0
  183. package/dist/src/schema.d.ts +13 -0
  184. package/dist/src/schema.js +36 -0
  185. package/dist/src/secrets.d.ts +10 -0
  186. package/dist/src/secrets.js +41 -0
  187. package/dist/src/shell.d.ts +34 -0
  188. package/dist/src/shell.js +95 -0
  189. package/dist/src/skills.d.ts +20 -0
  190. package/dist/src/skills.js +112 -0
  191. package/dist/src/snapshot.d.ts +20 -0
  192. package/dist/src/snapshot.js +93 -0
  193. package/dist/src/stats.d.ts +24 -0
  194. package/dist/src/stats.js +62 -0
  195. package/dist/src/strategies.d.ts +16 -0
  196. package/dist/src/strategies.js +35 -0
  197. package/dist/src/suites.d.ts +30 -0
  198. package/dist/src/suites.js +112 -0
  199. package/dist/src/trace.d.ts +63 -0
  200. package/dist/src/trace.js +153 -0
  201. package/dist/src/tune.d.ts +38 -0
  202. package/dist/src/tune.js +49 -0
  203. package/dist/src/web.d.ts +19 -0
  204. package/dist/src/web.js +56 -0
  205. package/dist/src/worktree.d.ts +24 -0
  206. package/dist/src/worktree.js +85 -0
  207. package/docs/agent-loop.md +222 -0
  208. package/docs/architecture.md +228 -0
  209. package/docs/bench.md +292 -0
  210. package/docs/budgets.md +185 -0
  211. package/docs/build-your-own-harness.md +424 -0
  212. package/docs/cookbook.md +375 -0
  213. package/docs/development.md +150 -0
  214. package/docs/evaluation.md +450 -0
  215. package/docs/features.md +1320 -0
  216. package/docs/getting-started.md +205 -0
  217. package/docs/headless.md +188 -0
  218. package/docs/images/approval.png +0 -0
  219. package/docs/images/chat.png +0 -0
  220. package/docs/images/daylight.png +0 -0
  221. package/docs/images/loop.png +0 -0
  222. package/docs/images/palette.png +0 -0
  223. package/docs/images/running.png +0 -0
  224. package/docs/images/subagent-done.png +0 -0
  225. package/docs/images/subagent-running.png +0 -0
  226. package/docs/images/welcome.png +0 -0
  227. package/docs/index.md +25 -0
  228. package/docs/library.md +267 -0
  229. package/docs/plugins.md +691 -0
  230. package/docs/providers.md +175 -0
  231. package/docs/safety.md +250 -0
  232. package/docs/usage.md +348 -0
  233. package/docs/why.md +155 -0
  234. package/docs/writing-guide.md +39 -0
  235. package/package.json +66 -4
@@ -0,0 +1,39 @@
1
+ export type Suggestion = {
2
+ label: string;
3
+ insert: string;
4
+ detail?: string;
5
+ };
6
+ export type Token = {
7
+ kind: "slash" | "file";
8
+ query: string;
9
+ start: number;
10
+ end: number;
11
+ };
12
+ /** What is the user completing right now? A slash command (first word of the input) or an @file mention. */
13
+ export declare function tokenAtCursor(text: string, cursor: number): Token | null;
14
+ export declare function rankCommands(query: string, cmds: {
15
+ name: string;
16
+ help: string;
17
+ alias?: string;
18
+ }[], limit?: number): Suggestion[];
19
+ /** Subsequence fuzzy match. Tries the greedy alignment and the exact-substring alignments and keeps the best. null = no match. */
20
+ export declare function fuzzyScore(query: string, path: string): number | null;
21
+ export declare function rankFiles(query: string, files: string[], limit?: number): Suggestion[];
22
+ export declare function applySuggestion(text: string, t: Token, s: Suggestion): {
23
+ text: string;
24
+ cursor: number;
25
+ };
26
+ /** Project file list: `git ls-files` when available (respects .gitignore), else a bounded directory walk. Refreshes at most every few seconds. */
27
+ export declare class FileIndex {
28
+ files: string[];
29
+ private at;
30
+ private loading;
31
+ cwd: string;
32
+ maxFiles: number;
33
+ constructor(cwd: string, maxFiles?: number);
34
+ /** Has the list been (re)built recently enough to trust? */
35
+ fresh(maxAgeMs?: number): boolean;
36
+ ensure(maxAgeMs?: number): Promise<void>;
37
+ private load;
38
+ private walk;
39
+ }
@@ -0,0 +1,113 @@
1
+ // Autocomplete for the input box: "/commands" and "@file" mentions. Pure ranking + a lazily built file index.
2
+ import { execFile } from "node:child_process";
3
+ import { readdirSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ /** What is the user completing right now? A slash command (first word of the input) or an @file mention. */
6
+ export function tokenAtCursor(text, cursor) {
7
+ const before = text.slice(0, cursor);
8
+ const slash = before.match(/^\/(\S*)$/);
9
+ if (slash)
10
+ return { kind: "slash", query: slash[1], start: 0, end: cursor };
11
+ const file = before.match(/(?:^|\s)@([^\s@]*)$/);
12
+ if (file)
13
+ return { kind: "file", query: file[1], start: cursor - file[1].length - 1, end: cursor };
14
+ return null;
15
+ }
16
+ export function rankCommands(query, cmds, limit = 8) {
17
+ const q = query.toLowerCase();
18
+ const score = (c) => (c.name.startsWith(q) ? 3 : c.alias?.startsWith(q) ? 2 : c.name.includes(q) || c.alias?.includes(q) ? 1 : 0);
19
+ return cmds.map((c) => ({ c, score: score(c) }))
20
+ .filter((x) => x.score > 0 || !q).sort((a, b) => b.score - a.score || a.c.name.localeCompare(b.c.name)).slice(0, limit)
21
+ .map(({ c }) => ({ label: "/" + c.name, insert: "/" + c.name + " ", detail: c.help + (c.alias && !c.name.includes(query.toLowerCase()) && c.alias.includes(query.toLowerCase()) ? ` (also /${c.alias})` : "") }));
22
+ }
23
+ /** Subsequence fuzzy match. Tries the greedy alignment and the exact-substring alignments and keeps the best. null = no match. */
24
+ export function fuzzyScore(query, path) {
25
+ const q = query.toLowerCase(), p = path.toLowerCase(), base = p.lastIndexOf("/") + 1;
26
+ if (!q)
27
+ return -path.length;
28
+ const score = (pos) => {
29
+ let s = 0, run = 0, prev = -2;
30
+ for (const i of pos) {
31
+ run = prev === i - 1 ? run + 1 : 1;
32
+ s += 1 + run * 2 + (i === 0 || "/._-".includes(p[i - 1]) ? 6 : 0) + (i >= base ? 4 : 0);
33
+ prev = i;
34
+ }
35
+ return s;
36
+ };
37
+ const greedy = [];
38
+ for (let i = 0, qi = 0; i < p.length && qi < q.length; i++)
39
+ if (p[i] === q[qi]) {
40
+ greedy.push(i);
41
+ qi++;
42
+ }
43
+ if (greedy.length < q.length)
44
+ return null;
45
+ const run = (at) => Array.from({ length: q.length }, (_, k) => at + k);
46
+ const inBase = p.indexOf(q, base), anywhere = p.indexOf(q);
47
+ let best = score(greedy);
48
+ if (anywhere >= 0)
49
+ best = Math.max(best, score(run(anywhere)) + 15);
50
+ if (inBase >= 0)
51
+ best = Math.max(best, score(run(inBase)) + 15 + (inBase === base ? 25 : 0));
52
+ return best - path.length * 0.05;
53
+ }
54
+ export function rankFiles(query, files, limit = 8) {
55
+ return files.map((f) => ({ f, s: fuzzyScore(query, f) })).filter((x) => x.s !== null)
56
+ .sort((a, b) => b.s - a.s || a.f.localeCompare(b.f)).slice(0, limit)
57
+ .map(({ f }) => ({ label: f, insert: "@" + f + " ", detail: undefined }));
58
+ }
59
+ export function applySuggestion(text, t, s) {
60
+ const next = text.slice(0, t.start) + s.insert + text.slice(t.end);
61
+ return { text: next, cursor: t.start + s.insert.length };
62
+ }
63
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__", ".venv", "target", ".amness", ".cache", "coverage"]);
64
+ /** Project file list: `git ls-files` when available (respects .gitignore), else a bounded directory walk. Refreshes at most every few seconds. */
65
+ export class FileIndex {
66
+ files = [];
67
+ at = 0;
68
+ loading = null;
69
+ cwd;
70
+ maxFiles;
71
+ constructor(cwd, maxFiles = 8000) { this.cwd = cwd; this.maxFiles = maxFiles; }
72
+ /** Has the list been (re)built recently enough to trust? */
73
+ fresh(maxAgeMs = 5000) { return Date.now() - this.at < maxAgeMs; }
74
+ ensure(maxAgeMs = 5000) {
75
+ if (Date.now() - this.at < maxAgeMs)
76
+ return Promise.resolve();
77
+ return (this.loading ??= this.load().finally(() => { this.loading = null; }));
78
+ }
79
+ load() {
80
+ return new Promise((done) => {
81
+ execFile("git", ["ls-files", "-co", "--exclude-standard"], { cwd: this.cwd, maxBuffer: 32 << 20 }, (err, out) => {
82
+ this.files = !err && out.trim() ? out.split("\n").filter(Boolean).slice(0, this.maxFiles) : this.walk();
83
+ this.at = Date.now();
84
+ done();
85
+ });
86
+ });
87
+ }
88
+ walk() {
89
+ const out = [];
90
+ const rec = (dir, rel) => {
91
+ let names = [];
92
+ try {
93
+ names = readdirSync(dir).sort();
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ for (const n of names) {
99
+ if (out.length >= this.maxFiles)
100
+ return;
101
+ if (SKIP.has(n) || (n.startsWith(".") && n !== ".github"))
102
+ continue;
103
+ const p = join(dir, n), r = rel ? rel + "/" + n : n;
104
+ try {
105
+ statSync(p).isDirectory() ? rec(p, r) : out.push(r);
106
+ }
107
+ catch { /* unreadable */ }
108
+ }
109
+ };
110
+ rec(this.cwd, "");
111
+ return out;
112
+ }
113
+ }
@@ -0,0 +1,9 @@
1
+ import type { Cmd } from "./commands.ts";
2
+ export declare function parseCommandFile(raw: string, fallback: string): {
3
+ description: string;
4
+ body: string;
5
+ };
6
+ export declare function loadCustomCommands(dirs: {
7
+ dir: string;
8
+ scope: "project" | "user";
9
+ }[]): Cmd[];
@@ -0,0 +1,34 @@
1
+ // User-defined slash commands: every <name>.md in .amness/commands (project) or ~/.amness/commands (user) becomes /<name>.
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ export function parseCommandFile(raw, fallback) {
5
+ const fm = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
6
+ const body = (fm ? fm[2] : raw).trim();
7
+ const desc = fm?.[1].match(/^description:\s*(.+)$/m)?.[1].trim() ?? body.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "").slice(0, 70) ?? fallback;
8
+ return { description: desc, body };
9
+ }
10
+ export function loadCustomCommands(dirs) {
11
+ const out = new Map();
12
+ for (const { dir, scope } of dirs) {
13
+ if (!existsSync(dir))
14
+ continue;
15
+ for (const f of readdirSync(dir).sort()) {
16
+ if (!f.endsWith(".md"))
17
+ continue;
18
+ const name = basename(f, ".md").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
19
+ if (!name || (out.has(name) && scope === "user"))
20
+ continue; // a project command overrides a user command of the same name
21
+ let parsed;
22
+ try {
23
+ parsed = parseCommandFile(readFileSync(join(dir, f), "utf8"), name);
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ const { description, body } = parsed;
29
+ out.set(name, { name, help: `${description} (${scope})`, args: "[args]",
30
+ run: (app, args) => app.turn(body.includes("$ARGUMENTS") ? body.split("$ARGUMENTS").join(args) : args ? `${body}\n\n${args}` : body) });
31
+ }
32
+ }
33
+ return [...out.values()];
34
+ }
@@ -0,0 +1,25 @@
1
+ import type { Theme } from "./themes.ts";
2
+ export type DiffLine = {
3
+ t: " " | "+" | "-" | "gap";
4
+ text: string;
5
+ a: number;
6
+ b: number;
7
+ };
8
+ export declare const langOf: (path: string) => string;
9
+ /** Parse the unified diff text produced by `unifiedDiff` into rows with old/new line numbers; hunks are separated by `gap` rows. */
10
+ export declare function parseDiff(diff: string): DiffLine[];
11
+ /** For each - line directly followed by an equal number of + lines, the differing middle of each pair (common prefix/suffix trimmed). */
12
+ export declare function wordDiffs(rows: DiffLine[]): Map<number, [number, number]>;
13
+ export type DiffOpts = {
14
+ width: number;
15
+ theme: Theme;
16
+ lang: string;
17
+ mode?: "stacked" | "split";
18
+ maxRows?: number;
19
+ };
20
+ /** Rows (ANSI strings, exactly `width` wide) for a diff body. `hidden` = rows not shown because of maxRows. */
21
+ export declare function renderDiffRows(diff: string, o: DiffOpts): {
22
+ rows: string[];
23
+ hidden: number;
24
+ total: number;
25
+ };
@@ -0,0 +1,130 @@
1
+ // Diff viewer: line numbers, +/- background tints, syntax highlighting, word-level change marks, and a split (side-by-side) layout.
2
+ import { highlightWith } from "./markdown.js";
3
+ import { B, F, fit } from "./rich.js";
4
+ const EXT = { ts: "ts", tsx: "tsx", js: "js", jsx: "jsx", mjs: "js", cjs: "js", py: "py", rb: "rb", go: "go", rs: "rs", java: "java", c: "c", h: "c", cpp: "cpp", cs: "cs", sh: "sh", bash: "sh", zsh: "sh", json: "json", yml: "yaml", yaml: "yaml", toml: "toml", md: "md", css: "css", html: "html", kt: "kt", swift: "swift" };
5
+ export const langOf = (path) => EXT[path.split(".").pop()?.toLowerCase() ?? ""] ?? "";
6
+ /** Parse the unified diff text produced by `unifiedDiff` into rows with old/new line numbers; hunks are separated by `gap` rows. */
7
+ export function parseDiff(diff) {
8
+ const out = [];
9
+ let a = 0, b = 0;
10
+ for (const l of diff.split("\n")) {
11
+ if (/^(---|\+\+\+)/.test(l))
12
+ continue;
13
+ const h = l.match(/^@@ -(\d+),?\d* \+(\d+)/);
14
+ if (h) {
15
+ if (out.length)
16
+ out.push({ t: "gap", text: "", a: +h[1], b: +h[2] });
17
+ a = +h[1];
18
+ b = +h[2];
19
+ continue;
20
+ }
21
+ if (!l)
22
+ continue;
23
+ const t = l[0], text = l.slice(1);
24
+ if (t === "+")
25
+ out.push({ t, text, a: 0, b: b++ });
26
+ else if (t === "-")
27
+ out.push({ t, text, a: a++, b: 0 });
28
+ else
29
+ out.push({ t: " ", text, a: a++, b: b++ });
30
+ }
31
+ return out;
32
+ }
33
+ /** For each - line directly followed by an equal number of + lines, the differing middle of each pair (common prefix/suffix trimmed). */
34
+ export function wordDiffs(rows) {
35
+ const marks = new Map();
36
+ for (let i = 0; i < rows.length;) {
37
+ if (rows[i].t !== "-") {
38
+ i++;
39
+ continue;
40
+ }
41
+ let d = i;
42
+ while (d < rows.length && rows[d].t === "-")
43
+ d++;
44
+ let p = d;
45
+ while (p < rows.length && rows[p].t === "+")
46
+ p++;
47
+ const nd = d - i, np = p - d;
48
+ if (nd === np && nd > 0)
49
+ for (let k = 0; k < nd; k++) {
50
+ const x = rows[i + k].text, y = rows[d + k].text;
51
+ let pre = 0;
52
+ while (pre < x.length && pre < y.length && x[pre] === y[pre])
53
+ pre++;
54
+ let suf = 0;
55
+ while (suf < x.length - pre && suf < y.length - pre && x[x.length - 1 - suf] === y[y.length - 1 - suf])
56
+ suf++;
57
+ if (pre + suf < Math.min(x.length, y.length) || x.length !== y.length) {
58
+ marks.set(i + k, [pre, x.length - suf]);
59
+ marks.set(d + k, [pre, y.length - suf]);
60
+ }
61
+ }
62
+ i = Math.max(p, i + 1);
63
+ }
64
+ return marks;
65
+ }
66
+ function paintCode(text, lang, t, bg, strong, mark) {
67
+ const hl = (k, s) => F(k === "plain" ? t.syn.plain : t.syn[k], s);
68
+ const seg = (s, bgc) => B(bgc, highlightWith(s, lang, hl));
69
+ if (!mark || mark[0] >= mark[1])
70
+ return seg(text, bg);
71
+ return seg(text.slice(0, mark[0]), bg) + seg(text.slice(mark[0], mark[1]), strong) + seg(text.slice(mark[1]), bg);
72
+ }
73
+ /** Rows (ANSI strings, exactly `width` wide) for a diff body. `hidden` = rows not shown because of maxRows. */
74
+ export function renderDiffRows(diff, o) {
75
+ const t = o.theme, rows = parseDiff(diff), marks = wordDiffs(rows), numW = Math.max(3, String(Math.max(0, ...rows.map((r) => Math.max(r.a, r.b)))).length);
76
+ const out = [];
77
+ const num = (n, fgc, bg) => B(bg, F(fgc, (n ? String(n) : "").padStart(numW) + " "));
78
+ if (o.mode === "split" && o.width >= 100) {
79
+ const lw = Math.floor((o.width - 1) / 2), rw = o.width - 1 - lw, sep = B(t.surface, F(t.border, "│"));
80
+ const pairs = [];
81
+ for (let i = 0; i < rows.length;) {
82
+ const r = rows[i];
83
+ if (r.t === "gap") {
84
+ pairs.push({ l: r, r });
85
+ i++;
86
+ continue;
87
+ }
88
+ if (r.t === " ") {
89
+ pairs.push({ l: r, r, li: i, ri: i });
90
+ i++;
91
+ continue;
92
+ }
93
+ let d = i;
94
+ const ds = [], as = [];
95
+ while (d < rows.length && rows[d].t === "-")
96
+ ds.push(d++);
97
+ while (d < rows.length && rows[d].t === "+")
98
+ as.push(d++);
99
+ for (let k = 0; k < Math.max(ds.length, as.length); k++)
100
+ pairs.push({ l: ds[k] !== undefined ? rows[ds[k]] : undefined, r: as[k] !== undefined ? rows[as[k]] : undefined, li: ds[k], ri: as[k] });
101
+ i = d;
102
+ }
103
+ const side = (line, idx, isLeft) => {
104
+ const half = isLeft ? lw : rw;
105
+ if (!line)
106
+ return fit("", half, t.surface);
107
+ if (line.t === "gap")
108
+ return fit(" " + F(t.faint, "⋯"), half, t.surface2);
109
+ const kind = line.t, bg = kind === "-" ? t.delBg : kind === "+" ? t.addBg : t.surface, strong = kind === "-" ? t.delStrong : t.addStrong;
110
+ const n = isLeft ? line.a : line.b, signFg = kind === "-" ? t.delFg : kind === "+" ? t.addFg : t.faint;
111
+ return fit(num(n, kind === " " ? t.faint : signFg, bg) + F(signFg, kind === " " ? " " : kind + " ") + paintCode(line.text, o.lang, t, bg, strong, idx !== undefined ? marks.get(idx) : undefined), half, bg);
112
+ };
113
+ for (const p of pairs)
114
+ out.push(side(p.l, p.li, true) + sep + side(p.r, p.ri, false));
115
+ }
116
+ else {
117
+ rows.forEach((r, i) => {
118
+ if (r.t === "gap") {
119
+ out.push(fit(F(t.faint, " ".repeat(numW * 2 + 2) + "⋯"), o.width, t.surface2));
120
+ return;
121
+ }
122
+ const bg = r.t === "-" ? t.delBg : r.t === "+" ? t.addBg : t.surface, strong = r.t === "-" ? t.delStrong : t.addStrong;
123
+ const gutter = num(r.a, r.t === "-" ? t.delFg : t.faint, bg) + num(r.b, r.t === "+" ? t.addFg : t.faint, bg);
124
+ const sign = F(r.t === "-" ? t.delFg : r.t === "+" ? t.addFg : t.faint, r.t === " " ? " " : r.t + " ");
125
+ out.push(fit(gutter + sign + paintCode(r.text, o.lang, t, bg, strong, marks.get(i)), o.width, bg));
126
+ });
127
+ }
128
+ const max = o.maxRows ?? Infinity;
129
+ return { rows: out.slice(0, max), hidden: Math.max(0, out.length - max), total: out.length };
130
+ }
@@ -0,0 +1,60 @@
1
+ export type Layout = {
2
+ rows: {
3
+ line: number;
4
+ start: number;
5
+ end: number;
6
+ }[];
7
+ cursorRow: number;
8
+ cursorCol: number;
9
+ };
10
+ export declare class Editor {
11
+ lines: string[];
12
+ row: number;
13
+ col: number;
14
+ width: number;
15
+ history: string[];
16
+ private hIdx;
17
+ private draft;
18
+ private goal;
19
+ private pastes;
20
+ private pasteN;
21
+ get text(): string;
22
+ get empty(): boolean;
23
+ get cursorIndex(): number;
24
+ setText(t: string, cursor?: "end" | number): void;
25
+ clear(): void;
26
+ insert(s: string): void;
27
+ /** Large pastes become a short placeholder (like Claude Code); `submit()` expands it back. */
28
+ paste(s: string): void;
29
+ newline(): void;
30
+ private prevIdx;
31
+ private nextIdx;
32
+ backspace(): void;
33
+ del(): void;
34
+ left(): void;
35
+ right(): void;
36
+ home(): void;
37
+ end(): void;
38
+ wordLeft(): void;
39
+ wordRight(): void;
40
+ deleteWordBack(): void;
41
+ killToEnd(): void;
42
+ killToStart(): void;
43
+ /** Soft-wrapped visual rows for a given content width, plus where the cursor lands. */
44
+ layout(width: number): Layout;
45
+ /** Move the cursor one visual row up/down. Returns false at the top/bottom edge (caller can then browse history). */
46
+ vertical(dir: -1 | 1): boolean;
47
+ /** ↑ at the top row recalls older input, ↓ at the bottom row newer input. Returns whether anything changed. */
48
+ historyPrev(): boolean;
49
+ historyNext(): boolean;
50
+ /** Final text with paste placeholders expanded. Records history and clears the buffer. */
51
+ submit(): string;
52
+ /** Lines to draw (prompt gutter included) and the cursor position inside them. */
53
+ render(width: number, prompt?: string, cont?: string): {
54
+ lines: string[];
55
+ cursor: {
56
+ row: number;
57
+ col: number;
58
+ };
59
+ };
60
+ }
@@ -0,0 +1,230 @@
1
+ // Multi-line input editor as a pure state machine (no terminal I/O), so every behaviour is unit-testable.
2
+ import { cellWidth, cpWidth } from "./text.js";
3
+ const PH = /\[Pasted text #(\d+) \+\d+ lines?\]/g;
4
+ const isWord = (ch) => /[\p{L}\p{N}_]/u.test(ch);
5
+ export class Editor {
6
+ lines = [""];
7
+ row = 0;
8
+ col = 0; // UTF-16 index into lines[row]
9
+ width = 80; // content width used for visual-line movement (set by render)
10
+ history = [];
11
+ hIdx = -1;
12
+ draft = "";
13
+ goal = null; // desired display column for vertical moves
14
+ pastes = new Map();
15
+ pasteN = 0;
16
+ get text() { return this.lines.join("\n"); }
17
+ get empty() { return this.text.length === 0; }
18
+ get cursorIndex() { return this.lines.slice(0, this.row).reduce((n, l) => n + l.length + 1, 0) + this.col; }
19
+ setText(t, cursor = "end") {
20
+ this.lines = t.split("\n");
21
+ this.goal = null;
22
+ if (cursor === "end") {
23
+ this.row = this.lines.length - 1;
24
+ this.col = this.lines[this.row].length;
25
+ }
26
+ else {
27
+ let i = cursor;
28
+ this.row = 0;
29
+ while (this.row < this.lines.length - 1 && i > this.lines[this.row].length) {
30
+ i -= this.lines[this.row].length + 1;
31
+ this.row++;
32
+ }
33
+ this.col = i;
34
+ }
35
+ }
36
+ clear() { this.setText(""); this.pastes.clear(); this.hIdx = -1; }
37
+ insert(s) {
38
+ s = s.replace(/\r\n?/g, "\n");
39
+ this.goal = null;
40
+ const parts = s.split("\n"), cur = this.lines[this.row], tail = cur.slice(this.col);
41
+ this.lines[this.row] = cur.slice(0, this.col) + parts[0];
42
+ if (parts.length === 1) {
43
+ this.lines[this.row] += tail;
44
+ this.col += parts[0].length;
45
+ return;
46
+ }
47
+ const mid = parts.slice(1, -1), last = parts.at(-1);
48
+ this.lines.splice(this.row + 1, 0, ...mid, last + tail);
49
+ this.row += parts.length - 1;
50
+ this.col = last.length;
51
+ }
52
+ /** Large pastes become a short placeholder (like Claude Code); `submit()` expands it back. */
53
+ paste(s) {
54
+ s = s.replace(/\r\n?/g, "\n");
55
+ const n = s.split("\n").length;
56
+ if (s.length > 600 || n > 8) {
57
+ const id = ++this.pasteN;
58
+ this.pastes.set(id, s);
59
+ this.insert(`[Pasted text #${id} +${n} line${n > 1 ? "s" : ""}]`);
60
+ }
61
+ else
62
+ this.insert(s);
63
+ }
64
+ newline() { this.insert("\n"); }
65
+ prevIdx(s, i) { if (i <= 0)
66
+ return 0; i--; if (i > 0 && s.charCodeAt(i) >= 0xdc00 && s.charCodeAt(i) <= 0xdfff)
67
+ i--; return i; }
68
+ nextIdx(s, i) { if (i >= s.length)
69
+ return s.length; const c = s.charCodeAt(i); return i + (c >= 0xd800 && c <= 0xdbff ? 2 : 1); }
70
+ backspace() {
71
+ this.goal = null;
72
+ if (this.col > 0) {
73
+ const before = this.lines[this.row].slice(0, this.col), m = before.match(/\[Pasted text #\d+ \+\d+ lines?\]$/); // delete a placeholder as one unit
74
+ const from = m ? this.col - m[0].length : this.prevIdx(before, this.col);
75
+ this.lines[this.row] = this.lines[this.row].slice(0, from) + this.lines[this.row].slice(this.col);
76
+ this.col = from;
77
+ }
78
+ else if (this.row > 0) {
79
+ const prev = this.lines[this.row - 1];
80
+ this.lines[this.row - 1] = prev + this.lines[this.row];
81
+ this.lines.splice(this.row, 1);
82
+ this.row--;
83
+ this.col = prev.length;
84
+ }
85
+ }
86
+ del() {
87
+ this.goal = null;
88
+ const l = this.lines[this.row];
89
+ if (this.col < l.length)
90
+ this.lines[this.row] = l.slice(0, this.col) + l.slice(this.nextIdx(l, this.col));
91
+ else if (this.row < this.lines.length - 1) {
92
+ this.lines[this.row] = l + this.lines[this.row + 1];
93
+ this.lines.splice(this.row + 1, 1);
94
+ }
95
+ }
96
+ left() { this.goal = null; if (this.col > 0)
97
+ this.col = this.prevIdx(this.lines[this.row], this.col);
98
+ else if (this.row > 0) {
99
+ this.row--;
100
+ this.col = this.lines[this.row].length;
101
+ } }
102
+ right() { this.goal = null; const l = this.lines[this.row]; if (this.col < l.length)
103
+ this.col = this.nextIdx(l, this.col);
104
+ else if (this.row < this.lines.length - 1) {
105
+ this.row++;
106
+ this.col = 0;
107
+ } }
108
+ home() { this.goal = null; this.col = 0; }
109
+ end() { this.goal = null; this.col = this.lines[this.row].length; }
110
+ wordLeft() {
111
+ this.goal = null;
112
+ if (this.col === 0)
113
+ return this.left();
114
+ const l = this.lines[this.row];
115
+ let i = this.col;
116
+ while (i > 0 && !isWord(l[i - 1]))
117
+ i--;
118
+ while (i > 0 && isWord(l[i - 1]))
119
+ i--;
120
+ this.col = i;
121
+ }
122
+ wordRight() {
123
+ this.goal = null;
124
+ const l = this.lines[this.row];
125
+ if (this.col >= l.length)
126
+ return this.right();
127
+ let i = this.col;
128
+ while (i < l.length && !isWord(l[i]))
129
+ i++;
130
+ while (i < l.length && isWord(l[i]))
131
+ i++;
132
+ this.col = i;
133
+ }
134
+ deleteWordBack() { const to = this.col; this.wordLeft(); this.lines[this.row] = this.lines[this.row].slice(0, this.col) + this.lines[this.row].slice(to); }
135
+ killToEnd() { this.goal = null; const l = this.lines[this.row]; if (this.col < l.length)
136
+ this.lines[this.row] = l.slice(0, this.col);
137
+ else
138
+ this.del(); }
139
+ killToStart() { this.goal = null; this.lines[this.row] = this.lines[this.row].slice(this.col); this.col = 0; }
140
+ /** Soft-wrapped visual rows for a given content width, plus where the cursor lands. */
141
+ layout(width) {
142
+ const w = Math.max(4, width), rows = [];
143
+ let cursorRow = 0, cursorCol = 0;
144
+ this.lines.forEach((line, li) => {
145
+ let start = 0, i = 0, vis = 0, lastSpace = -1;
146
+ const push = (end) => { rows.push({ line: li, start, end }); };
147
+ while (i < line.length) {
148
+ const cp = line.codePointAt(i), ch = String.fromCodePoint(cp), cw = cpWidth(cp);
149
+ if (vis + cw > w) {
150
+ const brk = lastSpace > start ? lastSpace + 1 : i;
151
+ push(brk);
152
+ start = brk;
153
+ i = brk;
154
+ vis = cellWidth(line.slice(start, i));
155
+ lastSpace = -1;
156
+ continue;
157
+ }
158
+ if (ch === " ")
159
+ lastSpace = i;
160
+ vis += cw;
161
+ i += ch.length;
162
+ }
163
+ push(line.length);
164
+ });
165
+ rows.forEach((r, idx) => {
166
+ if (r.line !== this.row)
167
+ return;
168
+ const last = idx === rows.length - 1 || rows[idx + 1].line !== r.line;
169
+ if (this.col >= r.start && (this.col < r.end || (last && this.col <= r.end))) {
170
+ cursorRow = idx;
171
+ cursorCol = cellWidth(this.lines[r.line].slice(r.start, this.col));
172
+ }
173
+ });
174
+ return { rows, cursorRow, cursorCol };
175
+ }
176
+ /** Move the cursor one visual row up/down. Returns false at the top/bottom edge (caller can then browse history). */
177
+ vertical(dir) {
178
+ const lay = this.layout(this.width), target = lay.cursorRow + dir;
179
+ if (target < 0 || target >= lay.rows.length)
180
+ return false;
181
+ this.goal ??= lay.cursorCol;
182
+ const r = lay.rows[target], text = this.lines[r.line];
183
+ let col = r.start, w = 0;
184
+ while (col < r.end) {
185
+ const cw = cpWidth(text.codePointAt(col));
186
+ if (w + cw > this.goal)
187
+ break;
188
+ w += cw;
189
+ col += text.codePointAt(col) > 0xffff ? 2 : 1;
190
+ }
191
+ const lastRowOfLine = target === lay.rows.length - 1 || lay.rows[target + 1].line !== r.line; // the cursor may sit after the final character only
192
+ this.row = r.line;
193
+ this.col = Math.min(col, lastRowOfLine ? r.end : Math.max(r.start, r.end - 1));
194
+ return true;
195
+ }
196
+ /** ↑ at the top row recalls older input, ↓ at the bottom row newer input. Returns whether anything changed. */
197
+ historyPrev() {
198
+ if (!this.history.length || this.hIdx + 1 >= this.history.length)
199
+ return false;
200
+ if (this.hIdx === -1)
201
+ this.draft = this.text;
202
+ this.hIdx++;
203
+ this.setText(this.history[this.hIdx]);
204
+ return true;
205
+ }
206
+ historyNext() {
207
+ if (this.hIdx < 0)
208
+ return false;
209
+ this.hIdx--;
210
+ this.setText(this.hIdx < 0 ? this.draft : this.history[this.hIdx]);
211
+ return true;
212
+ }
213
+ /** Final text with paste placeholders expanded. Records history and clears the buffer. */
214
+ submit() {
215
+ const out = this.text.replace(PH, (m, id) => this.pastes.get(Number(id)) ?? m).trim();
216
+ if (out && this.history[0] !== out)
217
+ this.history.unshift(out);
218
+ this.history = this.history.slice(0, 200);
219
+ this.clear();
220
+ return out;
221
+ }
222
+ /** Lines to draw (prompt gutter included) and the cursor position inside them. */
223
+ render(width, prompt = "❯ ", cont = " ") {
224
+ const gw = cellWidth(prompt), cw = Math.max(8, width - gw);
225
+ this.width = cw;
226
+ const lay = this.layout(cw);
227
+ const lines = lay.rows.map((r, i) => (i === 0 ? prompt : cont) + this.lines[r.line].slice(r.start, r.end).replace(/ $/, (m) => (r.end === this.lines[r.line].length ? m : m)));
228
+ return { lines, cursor: { row: lay.cursorRow, col: gw + lay.cursorCol } };
229
+ }
230
+ }