@giovannijecha/jecode 0.1.5-rc.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/bin/jecode.js +11 -0
  4. package/dist/atomic.js +78 -0
  5. package/dist/batch-view.js +17 -0
  6. package/dist/batch.js +100 -0
  7. package/dist/cli-info.js +40 -0
  8. package/dist/commands.js +171 -0
  9. package/dist/config.js +97 -0
  10. package/dist/controller.js +90 -0
  11. package/dist/credential-commands.js +134 -0
  12. package/dist/credential-safety.js +86 -0
  13. package/dist/credentials.js +124 -0
  14. package/dist/main.js +7 -0
  15. package/dist/ollama-settings-command.js +75 -0
  16. package/dist/prompt.js +29 -0
  17. package/dist/provider-commands.js +236 -0
  18. package/dist/provider-errors.js +10 -0
  19. package/dist/providers/anthropic-stream.js +111 -0
  20. package/dist/providers/anthropic-wire.js +79 -0
  21. package/dist/providers/anthropic.js +77 -0
  22. package/dist/providers/catalog.js +37 -0
  23. package/dist/providers/http.js +219 -0
  24. package/dist/providers/index.js +18 -0
  25. package/dist/providers/ollama-endpoint.js +40 -0
  26. package/dist/providers/ollama-stream.js +60 -0
  27. package/dist/providers/ollama-wire.js +95 -0
  28. package/dist/providers/ollama.js +87 -0
  29. package/dist/providers/openai-stream.js +48 -0
  30. package/dist/providers/openai-wire.js +112 -0
  31. package/dist/providers/openai.js +70 -0
  32. package/dist/providers/sse.js +81 -0
  33. package/dist/providers/stream-limits.js +11 -0
  34. package/dist/session.js +3 -0
  35. package/dist/settings-command.js +202 -0
  36. package/dist/settings.js +98 -0
  37. package/dist/start.js +47 -0
  38. package/dist/tools/args.js +38 -0
  39. package/dist/tools/fs.js +277 -0
  40. package/dist/tools/index.js +39 -0
  41. package/dist/tools/paths.js +122 -0
  42. package/dist/tools/search.js +213 -0
  43. package/dist/tools/shell.js +139 -0
  44. package/dist/tools/text-boundary.js +102 -0
  45. package/dist/tools/types.js +1 -0
  46. package/dist/transcript-export.js +11 -0
  47. package/dist/transcript.js +49 -0
  48. package/dist/tui/activity.js +11 -0
  49. package/dist/tui/app-input.js +155 -0
  50. package/dist/tui/app-state.js +17 -0
  51. package/dist/tui/app-workflows.js +105 -0
  52. package/dist/tui/app.js +215 -0
  53. package/dist/tui/approve.js +67 -0
  54. package/dist/tui/blocks.js +23 -0
  55. package/dist/tui/complete.js +54 -0
  56. package/dist/tui/components/command-menu.js +17 -0
  57. package/dist/tui/components/composer.js +47 -0
  58. package/dist/tui/components/dock.js +10 -0
  59. package/dist/tui/components/footer.js +21 -0
  60. package/dist/tui/components/menu.js +38 -0
  61. package/dist/tui/components/messages.js +43 -0
  62. package/dist/tui/components/misc.js +22 -0
  63. package/dist/tui/components/status.js +45 -0
  64. package/dist/tui/components/tool.js +85 -0
  65. package/dist/tui/components/types.js +1 -0
  66. package/dist/tui/editor.js +105 -0
  67. package/dist/tui/feedback.js +74 -0
  68. package/dist/tui/field.js +60 -0
  69. package/dist/tui/frame.js +33 -0
  70. package/dist/tui/input.js +52 -0
  71. package/dist/tui/keys.js +177 -0
  72. package/dist/tui/modal.js +24 -0
  73. package/dist/tui/overlay.js +93 -0
  74. package/dist/tui/picker.js +99 -0
  75. package/dist/tui/screen.js +94 -0
  76. package/dist/tui/scroll.js +7 -0
  77. package/dist/tui/session-view.js +49 -0
  78. package/dist/tui/transcript-view.js +134 -0
  79. package/dist/tui/turn.js +255 -0
  80. package/dist/tui/view.js +88 -0
  81. package/dist/tui/workspace.js +59 -0
  82. package/dist/types.js +9 -0
  83. package/dist/ui/diff.js +109 -0
  84. package/dist/ui/highlight.js +158 -0
  85. package/dist/ui/inline.js +39 -0
  86. package/dist/ui/markdown.js +147 -0
  87. package/dist/ui/render.js +232 -0
  88. package/dist/ui/table.js +127 -0
  89. package/dist/ui/terminal-text.js +42 -0
  90. package/dist/ui/theme.js +25 -0
  91. package/dist/ui/width.js +196 -0
  92. package/dist/usage.js +30 -0
  93. package/dist/user-data.js +29 -0
  94. package/package.json +56 -0
@@ -0,0 +1,196 @@
1
+ // How wide a string actually is on screen.
2
+ //
3
+ // A row is laid out in cells, not in characters, and `.length` counts UTF-16
4
+ // code units — wrong three ways at once. An emoji is two units and two cells,
5
+ // a CJK glyph is one unit and two cells, a combining accent is one unit and no
6
+ // cell at all. Every alignment in the UI depends on this file being right:
7
+ // the right-hand column, a ground band, the cursor. Nothing measures with
8
+ // `.length`.
9
+ /** Ranges the terminal draws two cells wide (East Asian Wide and Fullwidth). */
10
+ const WIDE = [
11
+ [0x1100, 0x115f],
12
+ [0x2e80, 0x303e],
13
+ [0x3041, 0x33ff],
14
+ [0x3400, 0x4dbf],
15
+ [0x4e00, 0x9fff],
16
+ [0xa000, 0xa4cf],
17
+ [0xa960, 0xa97f],
18
+ [0xac00, 0xd7a3],
19
+ [0xf900, 0xfaff],
20
+ [0xfe10, 0xfe19],
21
+ [0xfe30, 0xfe6f],
22
+ [0xff00, 0xff60],
23
+ [0xffe0, 0xffe6],
24
+ [0x1f300, 0x1f64f],
25
+ [0x1f680, 0x1f6ff],
26
+ [0x1f900, 0x1f9ff],
27
+ [0x1fa70, 0x1faff],
28
+ [0x20000, 0x3fffd],
29
+ ];
30
+ /** Ranges that occupy no cell of their own: they attach to what precedes. */
31
+ const ZERO = [
32
+ [0x0300, 0x036f],
33
+ [0x0483, 0x0489],
34
+ [0x0591, 0x05bd],
35
+ [0x0610, 0x061a],
36
+ [0x064b, 0x065f],
37
+ [0x0e31, 0x0e31],
38
+ [0x0e34, 0x0e3a],
39
+ [0x1ab0, 0x1aff],
40
+ [0x1dc0, 0x1dff],
41
+ [0x200b, 0x200f],
42
+ [0x20d0, 0x20ff],
43
+ [0xfe00, 0xfe0f],
44
+ [0xfe20, 0xfe2f],
45
+ ];
46
+ function inRanges(code, ranges) {
47
+ for (const [lo, hi] of ranges) {
48
+ if (code < lo)
49
+ return false;
50
+ if (code <= hi)
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ // The emoji presentation selector, built rather than typed: an invisible byte
56
+ // in source is a byte nobody reviews.
57
+ const VS16 = String.fromCodePoint(0xfe0f);
58
+ // Grapheme segmentation is in the standard library, so a family emoji built
59
+ // out of five code points and three joiners counts as the one thing the
60
+ // terminal actually draws.
61
+ const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
62
+ export function graphemes(text) {
63
+ const out = [];
64
+ for (const { segment } of SEGMENTER.segment(text))
65
+ out.push(segment);
66
+ return out;
67
+ }
68
+ /**
69
+ * Cells taken by one grapheme cluster.
70
+ *
71
+ * The base code point decides, with one exception: U+FE0F asks for the emoji
72
+ * presentation of a character that would otherwise be drawn narrow, and every
73
+ * terminal that honours it gives that glyph two cells.
74
+ */
75
+ export function charWidth(cluster) {
76
+ const code = cluster.codePointAt(0);
77
+ if (code === undefined)
78
+ return 0;
79
+ if (code < 0x20 || code === 0x7f)
80
+ return 0;
81
+ if (inRanges(code, ZERO))
82
+ return 0;
83
+ if (cluster.includes(VS16))
84
+ return 2;
85
+ return inRanges(code, WIDE) ? 2 : 1;
86
+ }
87
+ export function textWidth(text) {
88
+ let total = 0;
89
+ for (const cluster of SEGMENTER.segment(text))
90
+ total += charWidth(cluster.segment);
91
+ return total;
92
+ }
93
+ /** The longest prefix of `text` that fits in `cols` cells. */
94
+ export function clip(text, cols) {
95
+ if (cols <= 0)
96
+ return "";
97
+ let out = "";
98
+ let used = 0;
99
+ for (const { segment } of SEGMENTER.segment(text)) {
100
+ const w = charWidth(segment);
101
+ if (used + w > cols)
102
+ break;
103
+ out += segment;
104
+ used += w;
105
+ }
106
+ return out;
107
+ }
108
+ /**
109
+ * Fit `text` to `cols`, marking what was dropped.
110
+ *
111
+ * A row is recognized by where it begins and what it ends in, so a value too
112
+ * long to show keeps both of its ends and loses the middle. An unmarked cut
113
+ * reads as a whole shorter thing — a truncated path looks like a real path
114
+ * that happens to be elsewhere.
115
+ */
116
+ export function elide(text, cols) {
117
+ const width = textWidth(text);
118
+ if (width <= cols)
119
+ return text;
120
+ if (cols <= 1)
121
+ return clip("…", cols);
122
+ const keep = cols - 1;
123
+ const head = Math.ceil(keep / 2);
124
+ const tail = keep - head;
125
+ const clusters = graphemes(text);
126
+ let front = "";
127
+ let used = 0;
128
+ for (const cluster of clusters) {
129
+ const w = charWidth(cluster);
130
+ if (used + w > head)
131
+ break;
132
+ front += cluster;
133
+ used += w;
134
+ }
135
+ let back = "";
136
+ used = 0;
137
+ for (let i = clusters.length - 1; i >= 0; i--) {
138
+ const cluster = clusters[i];
139
+ const w = charWidth(cluster);
140
+ if (used + w > tail)
141
+ break;
142
+ back = cluster + back;
143
+ used += w;
144
+ }
145
+ return `${front}…${back}`;
146
+ }
147
+ /**
148
+ * Word wrap measured in cells, honouring existing newlines.
149
+ *
150
+ * A word wider than the row is broken rather than allowed to overflow: the
151
+ * screen has autowrap off, so an overflowing row is not ugly, it is *gone* —
152
+ * the terminal drops what does not fit and the user never learns it was there.
153
+ */
154
+ export function wrapText(text, max, continuation = "") {
155
+ if (max <= 0)
156
+ return [text];
157
+ // A continuation as wide as the row would leave no room for the text itself.
158
+ const lead = textWidth(continuation) >= max ? 0 : textWidth(continuation);
159
+ const out = [];
160
+ for (const source of text.split("\n")) {
161
+ let line = "";
162
+ let width = 0;
163
+ let first = true;
164
+ const flush = () => {
165
+ out.push(first || lead === 0 ? line : continuation + line);
166
+ first = false;
167
+ line = "";
168
+ width = 0;
169
+ };
170
+ const room = () => max - (first ? 0 : lead);
171
+ for (const word of source.split(" ")) {
172
+ const w = textWidth(word);
173
+ if (line !== "" && width + 1 + w > room())
174
+ flush();
175
+ if (w > room()) {
176
+ // Too long for any row: spend whole rows on it until it fits.
177
+ let rest = word;
178
+ while (textWidth(rest) > room()) {
179
+ const head = clip(rest, room() - (line === "" ? 0 : width + 1));
180
+ if (head === "")
181
+ break;
182
+ line = line === "" ? head : `${line} ${head}`;
183
+ rest = rest.slice(head.length);
184
+ flush();
185
+ }
186
+ line = rest;
187
+ width = textWidth(rest);
188
+ continue;
189
+ }
190
+ line = line === "" ? word : `${line} ${word}`;
191
+ width = line === word ? w : width + 1 + w;
192
+ }
193
+ flush();
194
+ }
195
+ return out;
196
+ }
package/dist/usage.js ADDED
@@ -0,0 +1,30 @@
1
+ // Provider-neutral token accounting for one in-memory session.
2
+ export function emptyUsage() {
3
+ return {
4
+ requests: 0,
5
+ lastInputTokens: 0,
6
+ inputTokens: 0,
7
+ outputTokens: 0,
8
+ cachedInputTokens: 0,
9
+ cacheWriteInputTokens: 0,
10
+ reasoningTokens: 0,
11
+ };
12
+ }
13
+ export function recordUsage(total, next) {
14
+ total.requests += 1;
15
+ total.lastInputTokens = next.inputTokens;
16
+ total.inputTokens += next.inputTokens;
17
+ total.outputTokens += next.outputTokens;
18
+ total.cachedInputTokens += next.cachedInputTokens;
19
+ total.cacheWriteInputTokens += next.cacheWriteInputTokens;
20
+ total.reasoningTokens += next.reasoningTokens;
21
+ }
22
+ export function formatTokens(value) {
23
+ if (value < 1_000)
24
+ return String(value);
25
+ if (value < 10_000)
26
+ return `${(value / 1_000).toFixed(1)}k`;
27
+ if (value < 1_000_000)
28
+ return `${Math.round(value / 1_000)}k`;
29
+ return `${(value / 1_000_000).toFixed(1)}m`;
30
+ }
@@ -0,0 +1,29 @@
1
+ // Canonical per-user storage. Nothing here belongs to a workspace.
2
+ import { homedir } from "node:os";
3
+ import * as path from "node:path";
4
+ export function userDataDir() {
5
+ const override = process.env["JECODE_HOME"]?.trim();
6
+ return path.resolve(override === undefined || override === "" ? path.join(homedir(), ".jecode") : override);
7
+ }
8
+ export function userDataPath(name) {
9
+ return path.join(userDataDir(), name);
10
+ }
11
+ export function userDataLabel(name) {
12
+ const file = userDataPath(name);
13
+ const relative = path.relative(homedir(), file);
14
+ return relative === ""
15
+ ? "~"
16
+ : relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
17
+ ? `~${path.sep}${relative}`
18
+ : file;
19
+ }
20
+ /** Previous config-directory location, read only while users move to ~/.jecode. */
21
+ export function legacyUserDataPath(name) {
22
+ const base = process.platform === "win32"
23
+ ? process.env["APPDATA"]?.trim()
24
+ : process.env["XDG_CONFIG_HOME"]?.trim() || path.join(homedir(), ".config");
25
+ if (base === undefined || base === "")
26
+ return undefined;
27
+ const legacy = path.resolve(base, "jecode", name);
28
+ return legacy === userDataPath(name) ? undefined : legacy;
29
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@giovannijecha/jecode",
3
+ "version": "0.1.5-rc.2",
4
+ "description": "An owned coding agent with zero external runtime dependencies.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/giovannijecha/jecode.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/giovannijecha/jecode/issues"
12
+ },
13
+ "homepage": "https://github.com/giovannijecha/jecode#readme",
14
+ "keywords": [
15
+ "coding-agent",
16
+ "terminal",
17
+ "tui",
18
+ "ollama",
19
+ "typescript"
20
+ ],
21
+ "type": "module",
22
+ "files": [
23
+ "bin/",
24
+ "dist/",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "bin": {
29
+ "jecode": "bin/jecode.js"
30
+ },
31
+ "engines": {
32
+ "node": ">=24"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "scripts": {
39
+ "prepack": "node dev/build-release.ts --quiet",
40
+ "build:release": "node dev/build-release.ts",
41
+ "start": "node src/main.ts",
42
+ "tui:lab": "node dev/tui-lab.ts",
43
+ "bench:transcript": "node dev/benchmark-transcript.ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "npm run build:release && node --test",
46
+ "coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",
47
+ "check:package": "node dev/check-package.ts",
48
+ "check:install": "node dev/check-installed-cli.ts",
49
+ "check": "npm run typecheck && npm run coverage && npm run check:package && npm run check:install"
50
+ },
51
+ "dependencies": {},
52
+ "devDependencies": {
53
+ "@types/node": "^26.4.0",
54
+ "typescript": "^5.9.3"
55
+ }
56
+ }