codeply-cli 0.2.0 → 0.3.1

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 (43) hide show
  1. package/THIRD_PARTY_NOTICES.md +63 -0
  2. package/bin/codeply.js +558 -370
  3. package/lib/agent.mjs +1627 -160
  4. package/lib/ai.js +1107 -387
  5. package/lib/apply-limit.js +33 -51
  6. package/lib/apply-patch.js +217 -0
  7. package/lib/arity-table.json +138 -0
  8. package/lib/arity.js +70 -0
  9. package/lib/auth.js +118 -112
  10. package/lib/commands.js +103 -0
  11. package/lib/config.js +392 -136
  12. package/lib/design-library/artifact-template.html +500 -0
  13. package/lib/design-library/assemble-artifact.mjs +21 -0
  14. package/lib/design-library/build.mjs +226 -0
  15. package/lib/design-library/cleanup.mjs +45 -0
  16. package/lib/design-library/index.json +22380 -0
  17. package/lib/design-library/make-table-data.mjs +39 -0
  18. package/lib/design-library/query.mjs +104 -0
  19. package/lib/diagnostics.js +192 -0
  20. package/lib/edit-engine.js +171 -14
  21. package/lib/github-agent.mjs +307 -0
  22. package/lib/history.js +75 -0
  23. package/lib/mcp.js +258 -0
  24. package/lib/native-tools.mjs +189 -0
  25. package/lib/oauth-connectors.js +478 -0
  26. package/lib/permissions.js +109 -0
  27. package/lib/plugins.js +353 -0
  28. package/lib/render-md.mjs +617 -617
  29. package/lib/server.mjs +457 -0
  30. package/lib/session-db.js +220 -0
  31. package/lib/share.js +168 -0
  32. package/lib/skills.js +80 -22
  33. package/lib/snapshot.js +146 -0
  34. package/lib/subagents.js +164 -0
  35. package/lib/terminal-hints.js +154 -0
  36. package/lib/textarea.mjs +258 -258
  37. package/lib/tools.mjs +1658 -79
  38. package/lib/tui.js +7 -7
  39. package/lib/tui.mjs +1086 -992
  40. package/lib/typecheck-worker.js +147 -0
  41. package/lib/web-tools.js +158 -0
  42. package/package.json +4 -3
  43. package/skills/premium-web-design/SKILL.md +154 -0
package/lib/textarea.mjs CHANGED
@@ -1,258 +1,258 @@
1
- /**
2
- * Multiline composer input.
3
- *
4
- * ink-text-input is single-line and submits on any \r, which also means pasting
5
- * a code snippet fires a submit at the first newline. This replaces it.
6
- *
7
- * Getting a real shift+enter out of a terminal is the awkward part: by default
8
- * both enter and shift+enter send the same byte (\r), so they are physically
9
- * indistinguishable. We enable the kitty keyboard protocol's disambiguation
10
- * flag on mount (\x1b[>1u), which makes modified enter arrive as CSI 13;<mod>u
11
- * while leaving ordinary typing alone. Terminals that don't speak it ignore the
12
- * sequence, so the fallbacks below carry them:
13
- *
14
- * shift+enter real, when the terminal supports CSI-u → newline
15
- * alt+enter arrives as ESC \r → newline
16
- * ctrl+j arrives as \n, always distinct from \r → newline
17
- * trailing \ backslash at end of line, then enter → newline
18
- * enter → submit
19
- *
20
- * Ink reports \r as key.return with input '\r'. \n parses as name 'enter', so
21
- * key.return is false — that difference is what makes ctrl+j detectable.
22
- */
23
- import React from 'react';
24
- import { Box, Text, useInput } from 'ink';
25
-
26
- const KITTY_ON = '\x1b[>1u';
27
- const KITTY_OFF = '\x1b[<u';
28
- const KITTY_QUERY = '\x1b[?u';
29
-
30
- // Replies the terminal sends back to our own queries. They arrive on stdin
31
- // exactly like typed text, so without this they get inserted into the draft.
32
- const TERMINAL_REPORT = /^\[[?0-9;]*[uRc]$/;
33
-
34
- const INVERSE = '\x1b[7m'; // the cursor block
35
- const INVERSE_OFF = '\x1b[27m';
36
- const GREY = '\x1b[90m'; // placeholder
37
- const GREY_OFF = '\x1b[39m';
38
-
39
- // CSI 13;<modifier>u — enter with any modifier held.
40
- const CSI_MODIFIED_ENTER = /^\[13;\d+u$/;
41
-
42
- /**
43
- * Ask the terminal for kitty key disambiguation, then ask whether it took.
44
- *
45
- * Windows Terminal does not implement this protocol (it has its own
46
- * win32-input-mode instead), so on Windows the query simply goes unanswered and
47
- * shift+enter stays physically identical to enter. That is why the UI reports
48
- * whichever newline key actually works rather than promising shift+enter
49
- * everywhere — see `modifiedEnterWorks` in tui.mjs.
50
- */
51
- export function enableModifiedEnter(stdout) {
52
- if (!stdout || !stdout.isTTY) return () => {};
53
- let restored = false;
54
- const restore = () => {
55
- if (restored) return;
56
- restored = true;
57
- try { stdout.write(KITTY_OFF); } catch {}
58
- };
59
- try {
60
- stdout.write(KITTY_ON);
61
- stdout.write(KITTY_QUERY); // a reply means the protocol is really supported
62
- } catch {}
63
- process.once('exit', restore);
64
- return restore;
65
- }
66
-
67
- /** True for a terminal's answer to one of our capability queries. */
68
- export function isTerminalReport(input) {
69
- return TERMINAL_REPORT.test(input);
70
- }
71
-
72
- /** Split a string into rendered rows, wrapping each logical line at `width`. */
73
- function layout(value, width) {
74
- const rows = [];
75
- const logical = value.split('\n');
76
- for (let li = 0; li < logical.length; li++) {
77
- const line = logical[li];
78
- let start = 0;
79
- do {
80
- const chunk = line.slice(start, start + width);
81
- rows.push({ line: li, start, text: chunk });
82
- start += width;
83
- } while (start < line.length);
84
- }
85
- return rows;
86
- }
87
-
88
- /** Absolute offset of the start of logical line `n`. */
89
- function lineOffset(value, n) {
90
- let offset = 0;
91
- const lines = value.split('\n');
92
- for (let i = 0; i < n; i++) offset += lines[i].length + 1;
93
- return offset;
94
- }
95
-
96
- export default function TextArea({
97
- value,
98
- onChange,
99
- onSubmit,
100
- placeholder = '',
101
- width = 60,
102
- focus = true,
103
- showCursor = true,
104
- // When the command palette is open it owns up/down for selection, so the
105
- // composer must not also move its cursor on those keys.
106
- allowVertical = true,
107
- }) {
108
- const [cursor, setCursor] = React.useState(value.length);
109
-
110
- // Keep the cursor inside the value when it changes from outside (e.g. a
111
- // command completion replacing the draft).
112
- React.useEffect(() => {
113
- setCursor((c) => Math.min(c, value.length));
114
- }, [value]);
115
-
116
- const apply = React.useCallback((next, nextCursor) => {
117
- onChange(next);
118
- setCursor(Math.max(0, Math.min(nextCursor, next.length)));
119
- }, [onChange]);
120
-
121
- const insert = React.useCallback((text) => {
122
- const next = value.slice(0, cursor) + text + value.slice(cursor);
123
- apply(next, cursor + text.length);
124
- }, [value, cursor, apply]);
125
-
126
- const newline = React.useCallback(() => insert('\n'), [insert]);
127
-
128
- useInput((input, key) => {
129
- if (!focus) return;
130
-
131
- // ── newline vs submit ──
132
- if (CSI_MODIFIED_ENTER.test(input)) { newline(); return; }
133
- // Capability replies must never land in the draft as literal text.
134
- if (TERMINAL_REPORT.test(input)) return;
135
- if (key.return && key.shift) { newline(); return; }
136
- if (!key.return && (input === '\r' || input === '\n')) { newline(); return; } // alt+enter, ctrl+j
137
-
138
- if (key.return) {
139
- // Trailing backslash is the universal fallback for terminals that send
140
- // the same byte for enter and shift+enter.
141
- if (value.slice(0, cursor).endsWith('\\')) {
142
- apply(value.slice(0, cursor - 1) + '\n' + value.slice(cursor), cursor);
143
- return;
144
- }
145
- onSubmit(value);
146
- return;
147
- }
148
-
149
- // ── multi-character input is a paste ──
150
- if (input.length > 1 && !key.ctrl && !key.meta) {
151
- insert(input.replace(/\r\n?/g, '\n'));
152
- return;
153
- }
154
-
155
- // ── editing ──
156
- if (key.backspace || (key.delete && !key.meta)) {
157
- // Ink reports the DEL byte (0x7f) as `delete`, which is what most
158
- // terminals actually send for the backspace key.
159
- if (cursor > 0) apply(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
160
- return;
161
- }
162
-
163
- if (key.ctrl) {
164
- switch (input) {
165
- case 'a': setCursor(lineOffset(value, value.slice(0, cursor).split('\n').length - 1)); return;
166
- case 'e': {
167
- const lines = value.split('\n');
168
- const li = value.slice(0, cursor).split('\n').length - 1;
169
- setCursor(lineOffset(value, li) + lines[li].length);
170
- return;
171
- }
172
- case 'k': {
173
- const rest = value.slice(cursor);
174
- const nl = rest.indexOf('\n');
175
- apply(value.slice(0, cursor) + (nl === -1 ? '' : rest.slice(nl)), cursor);
176
- return;
177
- }
178
- case 'u': {
179
- const li = value.slice(0, cursor).split('\n').length - 1;
180
- const start = lineOffset(value, li);
181
- apply(value.slice(0, start) + value.slice(cursor), start);
182
- return;
183
- }
184
- case 'w': {
185
- const before = value.slice(0, cursor);
186
- const trimmed = before.replace(/\s*\S+$/, '');
187
- apply(trimmed + value.slice(cursor), trimmed.length);
188
- return;
189
- }
190
- default: return;
191
- }
192
- }
193
-
194
- // ── movement ──
195
- if (key.leftArrow) { setCursor((c) => Math.max(0, c - 1)); return; }
196
- if (key.rightArrow) { setCursor((c) => Math.min(value.length, c + 1)); return; }
197
-
198
- if (key.upArrow || key.downArrow) {
199
- if (!allowVertical) return;
200
- const lines = value.split('\n');
201
- const li = value.slice(0, cursor).split('\n').length - 1;
202
- const col = cursor - lineOffset(value, li);
203
- const target = key.upArrow ? li - 1 : li + 1;
204
- if (target < 0 || target >= lines.length) return;
205
- setCursor(lineOffset(value, target) + Math.min(col, lines[target].length));
206
- return;
207
- }
208
-
209
- // ── plain character ──
210
- if (input && !key.meta && !key.ctrl && !key.tab && !key.escape) {
211
- insert(input);
212
- }
213
- }, { isActive: focus });
214
-
215
- // ── render ──
216
- const w = Math.max(8, width);
217
-
218
- if (value.length === 0) {
219
- return React.createElement(
220
- Text,
221
- null,
222
- showCursor && focus
223
- ? INVERSE + (placeholder[0] || ' ') + INVERSE_OFF +
224
- GREY + placeholder.slice(1) + GREY_OFF
225
- : GREY + placeholder + GREY_OFF
226
- );
227
- }
228
-
229
- const rows = layout(value, w);
230
- const lines = value.split('\n');
231
- const curLine = value.slice(0, cursor).split('\n').length - 1;
232
- const curCol = cursor - lineOffset(value, curLine);
233
-
234
- return React.createElement(
235
- Box,
236
- { flexDirection: 'column' },
237
- ...rows.map((row, i) => {
238
- const isCursorRow =
239
- focus && showCursor &&
240
- row.line === curLine &&
241
- curCol >= row.start &&
242
- (curCol < row.start + w || (row.start + w >= lines[row.line].length && curCol <= lines[row.line].length));
243
-
244
- if (!isCursorRow) {
245
- return React.createElement(Text, { key: i }, row.text || ' ');
246
- }
247
- const at = curCol - row.start;
248
- const before = row.text.slice(0, at);
249
- const under = row.text[at] ?? ' ';
250
- const after = row.text.slice(at + 1);
251
- return React.createElement(
252
- Text,
253
- { key: i },
254
- before + INVERSE + under + INVERSE_OFF + after
255
- );
256
- })
257
- );
258
- }
1
+ /**
2
+ * Multiline composer input.
3
+ *
4
+ * ink-text-input is single-line and submits on any \r, which also means pasting
5
+ * a code snippet fires a submit at the first newline. This replaces it.
6
+ *
7
+ * Getting a real shift+enter out of a terminal is the awkward part: by default
8
+ * both enter and shift+enter send the same byte (\r), so they are physically
9
+ * indistinguishable. We enable the kitty keyboard protocol's disambiguation
10
+ * flag on mount (\x1b[>1u), which makes modified enter arrive as CSI 13;<mod>u
11
+ * while leaving ordinary typing alone. Terminals that don't speak it ignore the
12
+ * sequence, so the fallbacks below carry them:
13
+ *
14
+ * shift+enter real, when the terminal supports CSI-u → newline
15
+ * alt+enter arrives as ESC \r → newline
16
+ * ctrl+j arrives as \n, always distinct from \r → newline
17
+ * trailing \ backslash at end of line, then enter → newline
18
+ * enter → submit
19
+ *
20
+ * Ink reports \r as key.return with input '\r'. \n parses as name 'enter', so
21
+ * key.return is false - that difference is what makes ctrl+j detectable.
22
+ */
23
+ import React from 'react';
24
+ import { Box, Text, useInput } from 'ink';
25
+
26
+ const KITTY_ON = '\x1b[>1u';
27
+ const KITTY_OFF = '\x1b[<u';
28
+ const KITTY_QUERY = '\x1b[?u';
29
+
30
+ // Replies the terminal sends back to our own queries. They arrive on stdin
31
+ // exactly like typed text, so without this they get inserted into the draft.
32
+ const TERMINAL_REPORT = /^\[[?0-9;]*[uRc]$/;
33
+
34
+ const INVERSE = '\x1b[7m'; // the cursor block
35
+ const INVERSE_OFF = '\x1b[27m';
36
+ const GREY = '\x1b[90m'; // placeholder
37
+ const GREY_OFF = '\x1b[39m';
38
+
39
+ // CSI 13;<modifier>u - enter with any modifier held.
40
+ const CSI_MODIFIED_ENTER = /^\[13;\d+u$/;
41
+
42
+ /**
43
+ * Ask the terminal for kitty key disambiguation, then ask whether it took.
44
+ *
45
+ * Windows Terminal does not implement this protocol (it has its own
46
+ * win32-input-mode instead), so on Windows the query simply goes unanswered and
47
+ * shift+enter stays physically identical to enter. That is why the UI reports
48
+ * whichever newline key actually works rather than promising shift+enter
49
+ * everywhere - see `modifiedEnterWorks` in tui.mjs.
50
+ */
51
+ export function enableModifiedEnter(stdout) {
52
+ if (!stdout || !stdout.isTTY) return () => {};
53
+ let restored = false;
54
+ const restore = () => {
55
+ if (restored) return;
56
+ restored = true;
57
+ try { stdout.write(KITTY_OFF); } catch {}
58
+ };
59
+ try {
60
+ stdout.write(KITTY_ON);
61
+ stdout.write(KITTY_QUERY); // a reply means the protocol is really supported
62
+ } catch {}
63
+ process.once('exit', restore);
64
+ return restore;
65
+ }
66
+
67
+ /** True for a terminal's answer to one of our capability queries. */
68
+ export function isTerminalReport(input) {
69
+ return TERMINAL_REPORT.test(input);
70
+ }
71
+
72
+ /** Split a string into rendered rows, wrapping each logical line at `width`. */
73
+ function layout(value, width) {
74
+ const rows = [];
75
+ const logical = value.split('\n');
76
+ for (let li = 0; li < logical.length; li++) {
77
+ const line = logical[li];
78
+ let start = 0;
79
+ do {
80
+ const chunk = line.slice(start, start + width);
81
+ rows.push({ line: li, start, text: chunk });
82
+ start += width;
83
+ } while (start < line.length);
84
+ }
85
+ return rows;
86
+ }
87
+
88
+ /** Absolute offset of the start of logical line `n`. */
89
+ function lineOffset(value, n) {
90
+ let offset = 0;
91
+ const lines = value.split('\n');
92
+ for (let i = 0; i < n; i++) offset += lines[i].length + 1;
93
+ return offset;
94
+ }
95
+
96
+ export default function TextArea({
97
+ value,
98
+ onChange,
99
+ onSubmit,
100
+ placeholder = '',
101
+ width = 60,
102
+ focus = true,
103
+ showCursor = true,
104
+ // When the command palette is open it owns up/down for selection, so the
105
+ // composer must not also move its cursor on those keys.
106
+ allowVertical = true,
107
+ }) {
108
+ const [cursor, setCursor] = React.useState(value.length);
109
+
110
+ // Keep the cursor inside the value when it changes from outside (e.g. a
111
+ // command completion replacing the draft).
112
+ React.useEffect(() => {
113
+ setCursor((c) => Math.min(c, value.length));
114
+ }, [value]);
115
+
116
+ const apply = React.useCallback((next, nextCursor) => {
117
+ onChange(next);
118
+ setCursor(Math.max(0, Math.min(nextCursor, next.length)));
119
+ }, [onChange]);
120
+
121
+ const insert = React.useCallback((text) => {
122
+ const next = value.slice(0, cursor) + text + value.slice(cursor);
123
+ apply(next, cursor + text.length);
124
+ }, [value, cursor, apply]);
125
+
126
+ const newline = React.useCallback(() => insert('\n'), [insert]);
127
+
128
+ useInput((input, key) => {
129
+ if (!focus) return;
130
+
131
+ // ── newline vs submit ──
132
+ if (CSI_MODIFIED_ENTER.test(input)) { newline(); return; }
133
+ // Capability replies must never land in the draft as literal text.
134
+ if (TERMINAL_REPORT.test(input)) return;
135
+ if (key.return && key.shift) { newline(); return; }
136
+ if (!key.return && (input === '\r' || input === '\n')) { newline(); return; } // alt+enter, ctrl+j
137
+
138
+ if (key.return) {
139
+ // Trailing backslash is the universal fallback for terminals that send
140
+ // the same byte for enter and shift+enter.
141
+ if (value.slice(0, cursor).endsWith('\\')) {
142
+ apply(value.slice(0, cursor - 1) + '\n' + value.slice(cursor), cursor);
143
+ return;
144
+ }
145
+ onSubmit(value);
146
+ return;
147
+ }
148
+
149
+ // ── multi-character input is a paste ──
150
+ if (input.length > 1 && !key.ctrl && !key.meta) {
151
+ insert(input.replace(/\r\n?/g, '\n'));
152
+ return;
153
+ }
154
+
155
+ // ── editing ──
156
+ if (key.backspace || (key.delete && !key.meta)) {
157
+ // Ink reports the DEL byte (0x7f) as `delete`, which is what most
158
+ // terminals actually send for the backspace key.
159
+ if (cursor > 0) apply(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
160
+ return;
161
+ }
162
+
163
+ if (key.ctrl) {
164
+ switch (input) {
165
+ case 'a': setCursor(lineOffset(value, value.slice(0, cursor).split('\n').length - 1)); return;
166
+ case 'e': {
167
+ const lines = value.split('\n');
168
+ const li = value.slice(0, cursor).split('\n').length - 1;
169
+ setCursor(lineOffset(value, li) + lines[li].length);
170
+ return;
171
+ }
172
+ case 'k': {
173
+ const rest = value.slice(cursor);
174
+ const nl = rest.indexOf('\n');
175
+ apply(value.slice(0, cursor) + (nl === -1 ? '' : rest.slice(nl)), cursor);
176
+ return;
177
+ }
178
+ case 'u': {
179
+ const li = value.slice(0, cursor).split('\n').length - 1;
180
+ const start = lineOffset(value, li);
181
+ apply(value.slice(0, start) + value.slice(cursor), start);
182
+ return;
183
+ }
184
+ case 'w': {
185
+ const before = value.slice(0, cursor);
186
+ const trimmed = before.replace(/\s*\S+$/, '');
187
+ apply(trimmed + value.slice(cursor), trimmed.length);
188
+ return;
189
+ }
190
+ default: return;
191
+ }
192
+ }
193
+
194
+ // ── movement ──
195
+ if (key.leftArrow) { setCursor((c) => Math.max(0, c - 1)); return; }
196
+ if (key.rightArrow) { setCursor((c) => Math.min(value.length, c + 1)); return; }
197
+
198
+ if (key.upArrow || key.downArrow) {
199
+ if (!allowVertical) return;
200
+ const lines = value.split('\n');
201
+ const li = value.slice(0, cursor).split('\n').length - 1;
202
+ const col = cursor - lineOffset(value, li);
203
+ const target = key.upArrow ? li - 1 : li + 1;
204
+ if (target < 0 || target >= lines.length) return;
205
+ setCursor(lineOffset(value, target) + Math.min(col, lines[target].length));
206
+ return;
207
+ }
208
+
209
+ // ── plain character ──
210
+ if (input && !key.meta && !key.ctrl && !key.tab && !key.escape) {
211
+ insert(input);
212
+ }
213
+ }, { isActive: focus });
214
+
215
+ // ── render ──
216
+ const w = Math.max(8, width);
217
+
218
+ if (value.length === 0) {
219
+ return React.createElement(
220
+ Text,
221
+ null,
222
+ showCursor && focus
223
+ ? INVERSE + (placeholder[0] || ' ') + INVERSE_OFF +
224
+ GREY + placeholder.slice(1) + GREY_OFF
225
+ : GREY + placeholder + GREY_OFF
226
+ );
227
+ }
228
+
229
+ const rows = layout(value, w);
230
+ const lines = value.split('\n');
231
+ const curLine = value.slice(0, cursor).split('\n').length - 1;
232
+ const curCol = cursor - lineOffset(value, curLine);
233
+
234
+ return React.createElement(
235
+ Box,
236
+ { flexDirection: 'column' },
237
+ ...rows.map((row, i) => {
238
+ const isCursorRow =
239
+ focus && showCursor &&
240
+ row.line === curLine &&
241
+ curCol >= row.start &&
242
+ (curCol < row.start + w || (row.start + w >= lines[row.line].length && curCol <= lines[row.line].length));
243
+
244
+ if (!isCursorRow) {
245
+ return React.createElement(Text, { key: i }, row.text || ' ');
246
+ }
247
+ const at = curCol - row.start;
248
+ const before = row.text.slice(0, at);
249
+ const under = row.text[at] ?? ' ';
250
+ const after = row.text.slice(at + 1);
251
+ return React.createElement(
252
+ Text,
253
+ { key: i },
254
+ before + INVERSE + under + INVERSE_OFF + after
255
+ );
256
+ })
257
+ );
258
+ }