agentweaver 0.1.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,427 @@
1
+ import blessed from "neo-blessed";
2
+ import { renderMarkdownToTerminal } from "./markdown.js";
3
+ import { setOutputAdapter, stripAnsi } from "./tui.js";
4
+ export class InteractiveUi {
5
+ options;
6
+ screen;
7
+ header;
8
+ summary;
9
+ sidebar;
10
+ log;
11
+ input;
12
+ footer;
13
+ help;
14
+ history;
15
+ historyIndex;
16
+ busy = false;
17
+ currentCommand = "idle";
18
+ focusedPane = "input";
19
+ constructor(options, history) {
20
+ this.options = options;
21
+ this.history = history;
22
+ this.historyIndex = history.length;
23
+ this.screen = blessed.screen({
24
+ smartCSR: true,
25
+ fullUnicode: true,
26
+ title: `AgentWeaver ${options.issueKey}`,
27
+ dockBorders: true,
28
+ autoPadding: false,
29
+ });
30
+ this.header = blessed.box({
31
+ parent: this.screen,
32
+ top: 0,
33
+ left: 0,
34
+ width: "100%",
35
+ height: 3,
36
+ tags: true,
37
+ padding: {
38
+ left: 1,
39
+ right: 1,
40
+ },
41
+ border: "line",
42
+ style: {
43
+ border: { fg: "green" },
44
+ fg: "white",
45
+ },
46
+ });
47
+ this.sidebar = blessed.box({
48
+ parent: this.screen,
49
+ top: 3,
50
+ left: 0,
51
+ width: "28%",
52
+ height: "100%-7",
53
+ tags: true,
54
+ label: " Commands ",
55
+ padding: {
56
+ left: 1,
57
+ right: 1,
58
+ },
59
+ border: "line",
60
+ scrollable: true,
61
+ alwaysScroll: true,
62
+ keys: true,
63
+ vi: true,
64
+ style: {
65
+ border: { fg: "cyan" },
66
+ fg: "white",
67
+ },
68
+ });
69
+ this.summary = blessed.box({
70
+ parent: this.screen,
71
+ top: 3,
72
+ left: "28%",
73
+ width: "72%",
74
+ height: 9,
75
+ tags: true,
76
+ label: " Task Summary ",
77
+ padding: {
78
+ left: 1,
79
+ right: 1,
80
+ },
81
+ border: "line",
82
+ scrollable: true,
83
+ alwaysScroll: true,
84
+ keys: true,
85
+ vi: true,
86
+ style: {
87
+ border: { fg: "green" },
88
+ fg: "white",
89
+ },
90
+ });
91
+ this.log = blessed.log({
92
+ parent: this.screen,
93
+ top: 12,
94
+ left: "28%",
95
+ width: "72%",
96
+ height: "100%-16",
97
+ tags: false,
98
+ label: " Activity ",
99
+ padding: {
100
+ left: 1,
101
+ right: 1,
102
+ },
103
+ border: "line",
104
+ scrollable: true,
105
+ alwaysScroll: true,
106
+ keys: true,
107
+ vi: true,
108
+ mouse: true,
109
+ scrollbar: {
110
+ ch: " ",
111
+ inverse: true,
112
+ },
113
+ style: {
114
+ border: { fg: "yellow" },
115
+ fg: "white",
116
+ },
117
+ });
118
+ this.input = blessed.textbox({
119
+ parent: this.screen,
120
+ bottom: 1,
121
+ left: 0,
122
+ width: "100%",
123
+ height: 3,
124
+ keys: true,
125
+ inputOnFocus: true,
126
+ mouse: true,
127
+ label: " command ",
128
+ padding: {
129
+ left: 1,
130
+ },
131
+ border: "line",
132
+ style: {
133
+ border: { fg: "magenta" },
134
+ fg: "white",
135
+ focus: {
136
+ border: { fg: "magenta" },
137
+ },
138
+ },
139
+ });
140
+ this.footer = blessed.box({
141
+ parent: this.screen,
142
+ bottom: 0,
143
+ left: 0,
144
+ width: "100%",
145
+ height: 1,
146
+ tags: true,
147
+ style: { fg: "gray" },
148
+ });
149
+ this.help = blessed.box({
150
+ parent: this.screen,
151
+ top: "center",
152
+ left: "center",
153
+ width: "70%",
154
+ height: "65%",
155
+ hidden: true,
156
+ tags: true,
157
+ label: " Help ",
158
+ padding: {
159
+ left: 1,
160
+ right: 1,
161
+ },
162
+ border: "line",
163
+ scrollable: true,
164
+ keys: true,
165
+ vi: true,
166
+ mouse: true,
167
+ style: {
168
+ border: { fg: "magenta" },
169
+ bg: "black",
170
+ fg: "white",
171
+ },
172
+ });
173
+ this.bindKeys();
174
+ this.renderStaticContent();
175
+ }
176
+ bindKeys() {
177
+ this.screen.key(["C-c", "q"], () => {
178
+ this.options.onExit();
179
+ });
180
+ this.screen.key(["f1", "?"], () => {
181
+ this.help.hidden = !this.help.hidden;
182
+ if (!this.help.hidden) {
183
+ this.help.focus();
184
+ }
185
+ else {
186
+ this.input.focus();
187
+ }
188
+ this.screen.render();
189
+ });
190
+ this.screen.key(["escape"], () => {
191
+ this.help.hide();
192
+ this.focusPane("input");
193
+ this.screen.render();
194
+ });
195
+ this.screen.key(["C-l"], () => {
196
+ this.log.setContent("");
197
+ this.appendLog("Log cleared.");
198
+ });
199
+ this.screen.key(["S-tab"], () => {
200
+ this.cycleFocus(-1);
201
+ });
202
+ this.screen.key(["tab"], () => {
203
+ if (this.focusedPane !== "input") {
204
+ this.cycleFocus(1);
205
+ }
206
+ });
207
+ this.screen.key(["C-j"], () => {
208
+ this.focusPane("log");
209
+ });
210
+ this.screen.key(["C-k"], () => {
211
+ this.focusPane("input");
212
+ });
213
+ this.log.key(["up"], () => {
214
+ this.log.scroll(-1);
215
+ this.screen.render();
216
+ });
217
+ this.log.key(["down"], () => {
218
+ this.log.scroll(1);
219
+ this.screen.render();
220
+ });
221
+ this.log.key(["pageup"], () => {
222
+ this.log.scroll(-(this.log.height - 2));
223
+ this.screen.render();
224
+ });
225
+ this.log.key(["pagedown"], () => {
226
+ this.log.scroll(this.log.height - 2);
227
+ this.screen.render();
228
+ });
229
+ this.log.key(["home"], () => {
230
+ this.log.setScroll(0);
231
+ this.screen.render();
232
+ });
233
+ this.log.key(["end"], () => {
234
+ this.log.setScrollPerc(100);
235
+ this.screen.render();
236
+ });
237
+ this.summary.key(["pageup"], () => {
238
+ this.summary.scroll(-(this.summary.height - 2));
239
+ this.screen.render();
240
+ });
241
+ this.summary.key(["pagedown"], () => {
242
+ this.summary.scroll(this.summary.height - 2);
243
+ this.screen.render();
244
+ });
245
+ this.sidebar.key(["pageup"], () => {
246
+ this.sidebar.scroll(-(this.sidebar.height - 2));
247
+ this.screen.render();
248
+ });
249
+ this.sidebar.key(["pagedown"], () => {
250
+ this.sidebar.scroll(this.sidebar.height - 2);
251
+ this.screen.render();
252
+ });
253
+ this.input.key(["up"], () => {
254
+ if (this.history.length === 0) {
255
+ return;
256
+ }
257
+ this.historyIndex = Math.max(0, this.historyIndex - 1);
258
+ this.input.setValue(this.history[this.historyIndex] ?? "");
259
+ this.screen.render();
260
+ });
261
+ this.input.key(["down"], () => {
262
+ if (this.history.length === 0) {
263
+ return;
264
+ }
265
+ this.historyIndex = Math.min(this.history.length, this.historyIndex + 1);
266
+ this.input.setValue(this.history[this.historyIndex] ?? "");
267
+ this.screen.render();
268
+ });
269
+ this.input.key(["tab"], () => {
270
+ const current = String(this.input.getValue() ?? "");
271
+ const hit = this.options.commands.find((item) => item.startsWith(current.trim()));
272
+ if (hit) {
273
+ this.input.setValue(hit);
274
+ this.screen.render();
275
+ }
276
+ });
277
+ this.input.key(["C-j"], () => {
278
+ this.focusPane("log");
279
+ });
280
+ this.input.key(["C-u"], () => {
281
+ this.focusPane("summary");
282
+ });
283
+ this.input.key(["C-h"], () => {
284
+ this.focusPane("sidebar");
285
+ });
286
+ this.input.key(["S-tab"], () => {
287
+ this.cycleFocus(-1);
288
+ });
289
+ this.log.key(["tab"], () => {
290
+ this.cycleFocus(1);
291
+ });
292
+ this.log.key(["S-tab"], () => {
293
+ this.cycleFocus(-1);
294
+ });
295
+ this.summary.key(["tab"], () => {
296
+ this.cycleFocus(1);
297
+ });
298
+ this.summary.key(["S-tab"], () => {
299
+ this.cycleFocus(-1);
300
+ });
301
+ this.sidebar.key(["tab"], () => {
302
+ this.cycleFocus(1);
303
+ });
304
+ this.sidebar.key(["S-tab"], () => {
305
+ this.cycleFocus(-1);
306
+ });
307
+ this.input.on("submit", async (value) => {
308
+ const line = value.trim();
309
+ this.input.clearValue();
310
+ this.screen.render();
311
+ if (!line || this.busy) {
312
+ return;
313
+ }
314
+ this.history.push(line);
315
+ this.historyIndex = this.history.length;
316
+ this.appendLog(`> ${line}`);
317
+ await this.options.onSubmit(line);
318
+ this.focusPane("input");
319
+ });
320
+ }
321
+ cycleFocus(direction) {
322
+ const panes = ["input", "log", "summary", "sidebar"];
323
+ const currentIndex = panes.indexOf(this.focusedPane);
324
+ const nextIndex = (currentIndex + direction + panes.length) % panes.length;
325
+ this.focusPane(panes[nextIndex] ?? "input");
326
+ }
327
+ focusPane(pane) {
328
+ this.focusedPane = pane;
329
+ this.header.style.border.fg = "green";
330
+ this.log.style.border.fg = pane === "log" ? "brightYellow" : "yellow";
331
+ this.summary.style.border.fg = pane === "summary" ? "brightGreen" : "green";
332
+ this.sidebar.style.border.fg = pane === "sidebar" ? "brightCyan" : "cyan";
333
+ this.input.style.border.fg = pane === "input" ? "brightMagenta" : "magenta";
334
+ if (pane === "input") {
335
+ this.input.focus();
336
+ }
337
+ else if (pane === "log") {
338
+ this.log.focus();
339
+ }
340
+ else if (pane === "summary") {
341
+ this.summary.focus();
342
+ }
343
+ else {
344
+ this.sidebar.focus();
345
+ }
346
+ this.footer.setContent(` Focus: ${pane} | Enter: run command | Tab/Shift+Tab: switch pane | Ctrl+J: log | Ctrl+K: input | PgUp/PgDn: scroll | ?: help | q: exit `);
347
+ this.screen.render();
348
+ }
349
+ renderStaticContent() {
350
+ this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
351
+ `cwd: ${this.options.cwd} current: ${this.currentCommand}`);
352
+ const summaryBody = this.options.summaryText.trim()
353
+ ? this.options.summaryText.trim()
354
+ : "Using existing Jira task file.\n\nUse /help to see commands.";
355
+ this.summary.setContent(renderMarkdownToTerminal(stripAnsi(summaryBody)));
356
+ this.sidebar.setContent([
357
+ this.options.commands.join("\n"),
358
+ "",
359
+ "Keys:",
360
+ "? / F1 help",
361
+ "Ctrl+L clear log",
362
+ "Tab complete",
363
+ "q / Ctrl+C exit",
364
+ ].join("\n"));
365
+ this.help.setContent(renderMarkdownToTerminal([
366
+ "AgentWeaver interactive mode",
367
+ "",
368
+ "Use slash commands in the input box:",
369
+ this.options.commands.join("\n"),
370
+ "",
371
+ "Keys:",
372
+ "Tab autocomplete command",
373
+ "Up/Down history",
374
+ "Ctrl+L clear log",
375
+ "? or F1 toggle help",
376
+ "Esc close help",
377
+ "q / Ctrl+C exit",
378
+ ].join("\n")));
379
+ this.footer.setContent(" Enter: run command | Tab: complete | Up/Down: history | ?: help | Ctrl+L: clear log | q: exit ");
380
+ }
381
+ createAdapter() {
382
+ return {
383
+ writeStdout: (text) => {
384
+ this.appendLog(stripAnsi(text).replace(/\r/g, ""));
385
+ },
386
+ writeStderr: (text) => {
387
+ this.appendLog(stripAnsi(text).replace(/\r/g, ""));
388
+ },
389
+ supportsTransientStatus: false,
390
+ supportsPassthrough: false,
391
+ };
392
+ }
393
+ mount() {
394
+ setOutputAdapter(this.createAdapter());
395
+ this.focusPane("input");
396
+ }
397
+ destroy() {
398
+ setOutputAdapter(null);
399
+ this.screen.destroy();
400
+ }
401
+ setBusy(busy, command) {
402
+ this.busy = busy;
403
+ this.currentCommand = command ?? (busy ? this.currentCommand : "idle");
404
+ this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
405
+ `cwd: ${this.options.cwd}\n` +
406
+ `current: ${this.currentCommand}${busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`);
407
+ this.input.setLabel(busy ? " command [busy] " : " command ");
408
+ this.screen.render();
409
+ }
410
+ appendLog(text) {
411
+ const normalized = text
412
+ .split("\n")
413
+ .map((line) => line.replace(/\t/g, " "))
414
+ .join("\n")
415
+ .trimEnd();
416
+ if (!normalized) {
417
+ this.log.add("");
418
+ }
419
+ else {
420
+ for (const line of normalized.split("\n")) {
421
+ this.log.add(line);
422
+ }
423
+ }
424
+ this.log.setScrollPerc(100);
425
+ this.screen.render();
426
+ }
427
+ }
package/dist/jira.js ADDED
@@ -0,0 +1,56 @@
1
+ import { existsSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { TaskRunnerError } from "./errors.js";
4
+ const ISSUE_KEY_RE = /^[A-Z][A-Z0-9_]*-[0-9]+$/;
5
+ export function extractIssueKey(jiraRef) {
6
+ const normalizedRef = jiraRef.replace(/\/+$/, "");
7
+ if (normalizedRef.includes("://")) {
8
+ const issueKey = normalizedRef.split("/").pop() ?? "";
9
+ if (!normalizedRef.includes("/browse/") || !issueKey) {
10
+ throw new TaskRunnerError("Expected Jira browse URL like https://jira.example.ru/browse/DEMO-3288");
11
+ }
12
+ return issueKey;
13
+ }
14
+ if (!ISSUE_KEY_RE.test(normalizedRef)) {
15
+ throw new TaskRunnerError("Expected Jira issue key like DEMO-3288 or browse URL like https://jira.example.ru/browse/DEMO-3288");
16
+ }
17
+ return normalizedRef;
18
+ }
19
+ export function buildJiraBrowseUrl(jiraRef) {
20
+ if (jiraRef.includes("://")) {
21
+ return jiraRef.replace(/\/+$/, "");
22
+ }
23
+ const baseUrl = process.env.JIRA_BASE_URL?.replace(/\/+$/, "") ?? "";
24
+ if (!baseUrl) {
25
+ throw new TaskRunnerError("JIRA_BASE_URL is required when passing only a Jira issue key.");
26
+ }
27
+ return `${baseUrl}/browse/${extractIssueKey(jiraRef)}`;
28
+ }
29
+ export function buildJiraApiUrl(jiraRef) {
30
+ const browseUrl = buildJiraBrowseUrl(jiraRef);
31
+ const issueKey = extractIssueKey(jiraRef);
32
+ const baseUrl = browseUrl.split("/browse/")[0];
33
+ return `${baseUrl}/rest/api/2/issue/${issueKey}`;
34
+ }
35
+ export async function fetchJiraIssue(jiraApiUrl, jiraTaskFile) {
36
+ const jiraApiKey = process.env.JIRA_API_KEY;
37
+ if (!jiraApiKey) {
38
+ throw new TaskRunnerError("JIRA_API_KEY is required for plan mode.");
39
+ }
40
+ const response = await fetch(jiraApiUrl, {
41
+ headers: {
42
+ Authorization: `Bearer ${jiraApiKey}`,
43
+ Accept: "application/json",
44
+ },
45
+ });
46
+ if (!response.ok) {
47
+ throw new TaskRunnerError(`Failed to fetch Jira issue: HTTP ${response.status}`);
48
+ }
49
+ const body = Buffer.from(await response.arrayBuffer());
50
+ await writeFile(jiraTaskFile, body);
51
+ }
52
+ export function requireJiraTaskFile(jiraTaskFile) {
53
+ if (!existsSync(jiraTaskFile)) {
54
+ throw new TaskRunnerError(`Jira issue JSON not found: ${jiraTaskFile}\nRun plan mode first to download the Jira task.`);
55
+ }
56
+ }
@@ -0,0 +1,183 @@
1
+ import MarkdownIt from "markdown-it";
2
+ const md = new MarkdownIt({
3
+ html: false,
4
+ linkify: true,
5
+ typographer: false,
6
+ });
7
+ function wrapText(text, width = 88, indent = "") {
8
+ const words = text.trim().split(/\s+/).filter(Boolean);
9
+ if (words.length === 0) {
10
+ return [indent];
11
+ }
12
+ const lines = [];
13
+ let current = indent;
14
+ for (const word of words) {
15
+ const candidate = current.trim() ? `${current} ${word}` : `${indent}${word}`;
16
+ if (candidate.length > width && current.trim()) {
17
+ lines.push(current);
18
+ current = `${indent}${word}`;
19
+ }
20
+ else {
21
+ current = candidate;
22
+ }
23
+ }
24
+ if (current) {
25
+ lines.push(current);
26
+ }
27
+ return lines;
28
+ }
29
+ function renderInline(tokens) {
30
+ if (!tokens) {
31
+ return "";
32
+ }
33
+ let result = "";
34
+ let linkHref = "";
35
+ for (const token of tokens) {
36
+ switch (token.type) {
37
+ case "text":
38
+ result += token.content;
39
+ break;
40
+ case "code_inline":
41
+ result += `\`${token.content}\``;
42
+ break;
43
+ case "softbreak":
44
+ case "hardbreak":
45
+ result += "\n";
46
+ break;
47
+ case "strong_open":
48
+ case "strong_close":
49
+ result += "*";
50
+ break;
51
+ case "em_open":
52
+ case "em_close":
53
+ result += "_";
54
+ break;
55
+ case "link_open":
56
+ linkHref = token.attrGet("href") ?? "";
57
+ break;
58
+ case "link_close":
59
+ if (linkHref) {
60
+ result += ` (${linkHref})`;
61
+ linkHref = "";
62
+ }
63
+ break;
64
+ case "image":
65
+ result += `[image: ${token.content || token.attrGet("src") || ""}]`;
66
+ break;
67
+ default:
68
+ if (token.content) {
69
+ result += token.content;
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+ export function renderMarkdownToTerminal(markdown, width = 88) {
77
+ const tokens = md.parse(markdown, {});
78
+ const lines = [];
79
+ let bulletDepth = 0;
80
+ let orderedDepth = 0;
81
+ let orderedIndex = 1;
82
+ let inBlockQuote = false;
83
+ for (let index = 0; index < tokens.length; index += 1) {
84
+ const token = tokens[index];
85
+ const previousType = tokens[index - 1]?.type ?? "";
86
+ const previousPreviousType = tokens[index - 2]?.type ?? "";
87
+ switch (token.type) {
88
+ case "heading_open": {
89
+ const inlineToken = tokens[index + 1];
90
+ const level = Number.parseInt(token.tag.replace("h", ""), 10);
91
+ const text = renderInline(inlineToken?.children).trim();
92
+ if (lines.length > 0) {
93
+ lines.push("");
94
+ }
95
+ const prefix = "#".repeat(Number.isNaN(level) ? 1 : level);
96
+ lines.push(`${prefix} ${text}`);
97
+ lines.push("");
98
+ break;
99
+ }
100
+ case "paragraph_open":
101
+ break;
102
+ case "inline": {
103
+ const isListItemInline = previousType === "list_item_open" ||
104
+ (previousType === "paragraph_open" && previousPreviousType === "list_item_open");
105
+ if (isListItemInline) {
106
+ const baseIndent = " ".repeat(Math.max(bulletDepth, orderedDepth) - 1);
107
+ const prefix = bulletDepth > 0 ? "• " : `${orderedIndex}. `;
108
+ const contentLines = wrapText(renderInline(token.children).trim(), width, `${baseIndent}${prefix}`);
109
+ lines.push(...contentLines);
110
+ if (orderedDepth > 0) {
111
+ orderedIndex += 1;
112
+ }
113
+ break;
114
+ }
115
+ const next = tokens[index + 1]?.type ?? "";
116
+ if (previousType === "heading_open" || previousType === "bullet_list_open" || previousType === "ordered_list_open") {
117
+ break;
118
+ }
119
+ const text = renderInline(token.children).trim();
120
+ if (!text) {
121
+ break;
122
+ }
123
+ const indent = inBlockQuote ? "> " : "";
124
+ lines.push(...wrapText(text, width, indent));
125
+ if (next === "paragraph_close") {
126
+ lines.push("");
127
+ }
128
+ break;
129
+ }
130
+ case "bullet_list_open":
131
+ bulletDepth += 1;
132
+ break;
133
+ case "bullet_list_close":
134
+ bulletDepth = Math.max(0, bulletDepth - 1);
135
+ lines.push("");
136
+ break;
137
+ case "ordered_list_open":
138
+ orderedDepth += 1;
139
+ orderedIndex = Number.parseInt(token.attrGet("start") ?? "1", 10) || 1;
140
+ break;
141
+ case "ordered_list_close":
142
+ orderedDepth = Math.max(0, orderedDepth - 1);
143
+ orderedIndex = 1;
144
+ lines.push("");
145
+ break;
146
+ case "list_item_open":
147
+ break;
148
+ case "list_item_close":
149
+ break;
150
+ case "blockquote_open":
151
+ inBlockQuote = true;
152
+ break;
153
+ case "blockquote_close":
154
+ inBlockQuote = false;
155
+ lines.push("");
156
+ break;
157
+ case "fence":
158
+ case "code_block": {
159
+ if (lines.length > 0 && lines[lines.length - 1] !== "") {
160
+ lines.push("");
161
+ }
162
+ const language = token.info?.trim();
163
+ lines.push(language ? `\`\`\` ${language}` : "```");
164
+ lines.push(...token.content.replace(/\n$/, "").split("\n"));
165
+ lines.push("```");
166
+ lines.push("");
167
+ break;
168
+ }
169
+ case "hr":
170
+ lines.push("─".repeat(Math.min(width, 40)));
171
+ lines.push("");
172
+ break;
173
+ case "paragraph_close":
174
+ break;
175
+ default:
176
+ break;
177
+ }
178
+ }
179
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
180
+ lines.pop();
181
+ }
182
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n");
183
+ }