jules-orchestrator-kit 0.52.8 → 0.53.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.
@@ -1,179 +0,0 @@
1
- import { listCommandsByCategory } from "../ops/command-registry.mjs";
2
- import { clipText } from "./layout.mjs";
3
- import { getSymbols } from "./capabilities.mjs";
4
-
5
- /**
6
- * @typedef {import("../ops/command-registry.mjs").CommandDescriptor} CommandDescriptor
7
- * @typedef {import("../ops/command-registry.mjs").CommandCategory} CommandCategory
8
- * @typedef {import("./capabilities.mjs").TerminalCapabilities} TerminalCapabilities
9
- * @typedef {import("./renderer.mjs").RenderFrame} RenderFrame
10
- */
11
-
12
- /**
13
- * Create command palette state manager.
14
- * @param {Object} [opts]
15
- * @param {CommandCategory} [opts.initialCategory]
16
- */
17
- export function createPaletteState(opts = {}) {
18
- let filter = "";
19
- let category = opts.initialCategory || null;
20
- let selectedIndex = 0;
21
-
22
- function getFilteredCommands() {
23
- let pool = listCommandsByCategory(category || undefined);
24
- if (!filter || !filter.trim()) return pool;
25
-
26
- const tokens = filter.trim().toLowerCase().split(/\s+/);
27
- return pool.filter((cmd) => {
28
- const text = `${cmd.id} ${cmd.title} ${cmd.description} ${cmd.category} ${cmd.shortcuts.join(" ")}`.toLowerCase();
29
- return tokens.every((token) => text.includes(token));
30
- });
31
- }
32
-
33
- function clampSelection() {
34
- const list = getFilteredCommands();
35
- if (list.length === 0) {
36
- selectedIndex = 0;
37
- return;
38
- }
39
- if (selectedIndex < 0) selectedIndex = 0;
40
- if (selectedIndex >= list.length) selectedIndex = list.length - 1;
41
- }
42
-
43
- clampSelection();
44
-
45
- return {
46
- get filter() {
47
- return filter;
48
- },
49
- set filter(val) {
50
- filter = String(val);
51
- selectedIndex = 0;
52
- clampSelection();
53
- },
54
- get category() {
55
- return category;
56
- },
57
- set category(val) {
58
- category = val;
59
- selectedIndex = 0;
60
- clampSelection();
61
- },
62
- get selectedIndex() {
63
- return selectedIndex;
64
- },
65
-
66
- move(delta) {
67
- const list = getFilteredCommands();
68
- if (list.length === 0) return;
69
- selectedIndex += delta;
70
- clampSelection();
71
- },
72
-
73
- nextCategory() {
74
- const categories = [null, "Create", "Inspect", "Operate", "Repair", "Configure"];
75
- const currentIdx = categories.indexOf(category);
76
- const nextIdx = (currentIdx + 1) % categories.length;
77
- this.category = categories[nextIdx];
78
- },
79
-
80
- getSelectedCommand() {
81
- const list = getFilteredCommands();
82
- return list[selectedIndex] || null;
83
- },
84
-
85
- getFilteredCommands,
86
- };
87
- }
88
-
89
- /**
90
- * Render native command palette view frame.
91
- * @param {ReturnType<typeof createPaletteState>} state
92
- * @param {TerminalCapabilities} caps
93
- * @returns {RenderFrame}
94
- */
95
- export function renderPaletteView(state, caps) {
96
- const width = caps.columns || 80;
97
- const height = caps.rows || 24;
98
- const symbols = getSymbols(caps);
99
-
100
- const lines = [];
101
-
102
- // Header line
103
- const titleStr = " agentctl · Command Palette ";
104
- lines.push({
105
- runs: [{ text: clipText(titleStr, width), style: { fg: "cyan", bold: true, inverse: true } }],
106
- });
107
-
108
- // Filter input line
109
- const promptStr = ` > ${state.filter}${symbols.bullet}`;
110
- lines.push({
111
- runs: [{ text: clipText(promptStr, width), style: { fg: "yellow" } }],
112
- });
113
-
114
- // Category tabs line
115
- const categories = [
116
- { label: "All", value: null },
117
- { label: "Create", value: "Create" },
118
- { label: "Inspect", value: "Inspect" },
119
- { label: "Operate", value: "Operate" },
120
- { label: "Repair", value: "Repair" },
121
- { label: "Configure", value: "Configure" },
122
- ];
123
-
124
- const tabRuns = [];
125
- for (const cat of categories) {
126
- const isSelected = state.category === cat.value;
127
- const label = `[${cat.label}] `;
128
- if (isSelected) {
129
- tabRuns.push({ text: label, style: { fg: "cyan", bold: true, underline: true } });
130
- } else {
131
- tabRuns.push({ text: label, style: { fg: "white", dim: true } });
132
- }
133
- }
134
- lines.push({ runs: tabRuns });
135
-
136
- // Command items body
137
- const filtered = state.getFilteredCommands();
138
- const maxBodyLines = height - 5;
139
-
140
- if (filtered.length === 0) {
141
- lines.push({
142
- runs: [{ text: " No matching commands found.", style: { fg: "yellow", dim: true } }],
143
- });
144
- } else {
145
- for (let i = 0; i < Math.min(filtered.length, maxBodyLines); i++) {
146
- const cmd = filtered[i];
147
- const isSelected = i === state.selectedIndex;
148
-
149
- const pointer = isSelected ? `${symbols.select} ` : " ";
150
- const catBadge = `[${cmd.category.padEnd(9)}] `;
151
- const cmdTitle = cmd.title.padEnd(16);
152
- const desc = cmd.description;
153
-
154
- const lineText = `${pointer}${catBadge}${cmdTitle} ${desc}`;
155
- const style = isSelected ? { fg: "cyan", bold: true } : { fg: "default" };
156
-
157
- lines.push({
158
- runs: [{ text: clipText(lineText, width), style }],
159
- });
160
- }
161
- }
162
-
163
- // Pad remaining height
164
- while (lines.length < height - 1) {
165
- lines.push({ runs: [{ text: " ".repeat(width), style: { fg: "default" } }] });
166
- }
167
-
168
- // Footer status bar line
169
- lines.push({
170
- runs: [
171
- {
172
- text: clipText("Up/Down: move Enter: select Tab: category Esc: quit", width),
173
- style: { fg: "cyan", dim: true },
174
- },
175
- ],
176
- });
177
-
178
- return { width, height, lines };
179
- }
@@ -1,206 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { createHash } from "node:crypto";
4
- import { parseEnvelopeHeader } from "../envelope.mjs";
5
- import { isPidAlive } from "../state.mjs";
6
-
7
- /**
8
- * @typedef {"pending" | "dispatching" | "remote-queued" | "planning" | "awaiting-plan-approval" | "awaiting-user-feedback" | "running" | "output-ready" | "verifying" | "deferred" | "quarantined" | "failed" | "completed" | "cancelled"} TaskState
9
- *
10
- * @typedef {Object} QueueTaskSummary
11
- * @property {string} id
12
- * @property {string} title
13
- * @property {TaskState} state
14
- * @property {string} riskTier
15
- * @property {number} attempt
16
- * @property {number} maxAttempts
17
- * @property {string} createdAt
18
- * @property {string} updatedAt
19
- * @property {number} durationMs
20
- * @property {string} [source]
21
- * @property {string} [startingBranch]
22
- * @property {boolean} autoPr
23
- * @property {boolean} requirePlanApproval
24
- * @property {string} [verifyCmd]
25
- * @property {string} [slotId]
26
- * @property {number} [pid]
27
- * @property {string} [worktree]
28
- * @property {string} [errorSummary]
29
- * @property {string} envelopeHash
30
- * @property {string | null} stateHash
31
- * @property {number} stateRevision
32
- * @property {string} taskFile
33
- */
34
-
35
- function isQueueTaskFile(fileName, content = "") {
36
- if (!fileName || fileName.toLowerCase() === "readme.md" || !fileName.toLowerCase().endsWith(".md")) {
37
- return false;
38
- }
39
- if (/^task-.*\.md$/i.test(fileName)) return true;
40
- return Boolean(parseEnvelopeHeader(content) || content.includes("# Task ID:"));
41
- }
42
-
43
- /**
44
- * @typedef {Object} QueueSnapshot
45
- * @property {"agentctl/queue-snapshot-v1"} schema
46
- * @property {string} repository
47
- * @property {string} generatedAt
48
- * @property {string} revision
49
- * @property {Record<TaskState, number>} counts
50
- * @property {QueueTaskSummary[]} tasks
51
- * @property {string[]} warnings
52
- */
53
-
54
- /**
55
- * Build immutable QueueSnapshot by reading canonical .agent/jules-queue/ tasks and sidecars.
56
- * @param {string} root
57
- * @param {Object} [options]
58
- * @param {boolean} [options.includeCompleted=true]
59
- * @param {number} [options.limit=100]
60
- * @returns {Promise<QueueSnapshot>}
61
- */
62
- export async function buildQueueSnapshot(root, options = {}) {
63
- const repoRoot = resolve(root || process.cwd());
64
- const queueDir = join(repoRoot, ".agent", "jules-queue");
65
- const warnings = [];
66
- const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : 100;
67
- const includeCompleted = options.includeCompleted !== false;
68
- /** @type {QueueTaskSummary[]} */
69
- const tasks = [];
70
-
71
- const counts = {
72
- pending: 0,
73
- dispatching: 0,
74
- "remote-queued": 0,
75
- planning: 0,
76
- "awaiting-plan-approval": 0,
77
- "awaiting-user-feedback": 0,
78
- running: 0,
79
- "output-ready": 0,
80
- verifying: 0,
81
- deferred: 0,
82
- quarantined: 0,
83
- failed: 0,
84
- completed: 0,
85
- cancelled: 0,
86
- };
87
-
88
- if (!existsSync(queueDir)) {
89
- warnings.push("Canonical queue directory .agent/jules-queue does not exist.");
90
- } else {
91
- try {
92
- const files = readdirSync(queueDir).filter((f) => f.toLowerCase().endsWith(".md"));
93
- for (const file of files) {
94
- const taskId = file.replace(/\.md$/, "");
95
- const taskPath = join(queueDir, file);
96
- const statePath = join(queueDir, `${taskId}.state.json`);
97
-
98
- try {
99
- const content = readFileSync(taskPath, "utf-8");
100
- if (!isQueueTaskFile(file, content)) continue;
101
- const envelope = parseEnvelopeHeader(content) || {};
102
- const envelopeHash = "sha256:" + createHash("sha256").update(content).digest("hex");
103
-
104
- let sidecar = {};
105
- let stateHash = null;
106
- if (existsSync(statePath)) {
107
- try {
108
- const stateContent = readFileSync(statePath, "utf-8");
109
- stateHash = "sha256:" + createHash("sha256").update(stateContent).digest("hex");
110
- sidecar = JSON.parse(stateContent);
111
- } catch {
112
- warnings.push(`Malformed sidecar file for task ${taskId}`);
113
- }
114
- }
115
-
116
- const knownStates = new Set([
117
- "pending", "dispatching", "remote-queued", "planning", "awaiting-plan-approval",
118
- "awaiting-user-feedback", "running", "output-ready", "verifying", "deferred",
119
- "quarantined", "failed", "completed", "cancelled",
120
- ]);
121
- let state = knownStates.has(sidecar.state) ? sidecar.state : "pending";
122
- if (sidecar.state && state === "pending" && sidecar.state !== "pending") {
123
- warnings.push(`Unknown state '${sidecar.state}' for task ${taskId}; treating it as pending`);
124
- }
125
- if ((state === "running" || state === "verifying") && sidecar.pid !== undefined && !isPidAlive(sidecar.pid)) {
126
- state = "failed";
127
- warnings.push(`Task ${taskId} references dead PID ${sidecar.pid}; treating it as failed`);
128
- }
129
- counts[state] = (counts[state] || 0) + 1;
130
-
131
- if (!includeCompleted && (state === "completed" || state === "cancelled")) {
132
- continue;
133
- }
134
-
135
- tasks.push({
136
- id: taskId,
137
- title: envelope.title || taskId,
138
- state,
139
- riskTier: envelope.riskTier || "R1",
140
- attempt: sidecar.attempt ?? 0,
141
- maxAttempts: sidecar.maxAttempts ?? envelope.maxAttempts ?? 3,
142
- createdAt: sidecar.createdAt || envelope.createdAt || new Date().toISOString(),
143
- updatedAt: sidecar.updatedAt || new Date().toISOString(),
144
- durationMs: sidecar.durationMs ?? 0,
145
- source: envelope.source,
146
- startingBranch: envelope.startingBranch,
147
- autoPr: Boolean(envelope.autoPr),
148
- requirePlanApproval: Boolean(envelope.requirePlanApproval),
149
- verifyCmd: envelope.verifyCmd,
150
- slotId: sidecar.slotId,
151
- pid: sidecar.pid,
152
- worktree: sidecar.worktree,
153
- errorSummary: sidecar.errorSummary,
154
- envelopeHash,
155
- stateHash,
156
- stateRevision: sidecar.revision ?? 1,
157
- taskFile: `.agent/jules-queue/${file}`,
158
- });
159
- } catch (err) {
160
- warnings.push(`Failed to read task ${taskId}: ${err.message}`);
161
- }
162
- }
163
- } catch (err) {
164
- warnings.push(`Failed to list queue directory: ${err.message}`);
165
- }
166
- }
167
-
168
- // Sort tasks by updatedAt descending
169
- tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
170
-
171
- const generatedAt = new Date().toISOString();
172
- const revisionPayload = JSON.stringify({ repository: repoRoot, generatedAt, counts, taskCount: tasks.length });
173
- const revision = "sha256:" + createHash("sha256").update(revisionPayload).digest("hex");
174
-
175
- return {
176
- schema: "agentctl/queue-snapshot-v1",
177
- repository: repoRoot,
178
- generatedAt,
179
- revision,
180
- counts,
181
- tasks: tasks.slice(0, limit),
182
- warnings,
183
- };
184
- }
185
-
186
- /**
187
- * Queue state reducer for updating view navigation state.
188
- * @param {Record<string, any>} state
189
- * @param {import("../key-decoder.mjs").KeyEvent | { type: string, payload?: any }} event
190
- * @returns {Record<string, any>}
191
- */
192
- export function reduceQueueState(state, event) {
193
- const nextState = { ...state };
194
- if (!event) return nextState;
195
-
196
- if (typeof event === "object" && "type" in event) {
197
- if (event.type === "SET_FILTER") {
198
- nextState.filter = String(event.payload || "");
199
- nextState.selectedIndex = 0;
200
- } else if (event.type === "SELECT_TASK") {
201
- nextState.selectedTaskId = String(event.payload || "");
202
- }
203
- }
204
-
205
- return nextState;
206
- }
@@ -1,188 +0,0 @@
1
- import { getStringWidth, clipText, stripAnsi } from "./layout.mjs";
2
-
3
- /**
4
- * @typedef {Object} TextStyle
5
- * @property {"default" | "red" | "green" | "yellow" | "blue" | "cyan" | "white"} [fg]
6
- * @property {"default" | "blue" | "cyan" | "bgRed" | "bgGreen"} [bg]
7
- * @property {boolean} [bold]
8
- * @property {boolean} [dim]
9
- * @property {boolean} [underline]
10
- * @property {boolean} [inverse]
11
- */
12
-
13
- /**
14
- * @typedef {Object} CellRun
15
- * @property {string} text
16
- * @property {TextStyle} style
17
- */
18
-
19
- /**
20
- * @typedef {Object} ScreenLine
21
- * @property {CellRun[]} runs
22
- * @property {string} [key]
23
- */
24
-
25
- /**
26
- * @typedef {Object} RenderFrame
27
- * @property {number} width
28
- * @property {number} height
29
- * @property {ScreenLine[]} lines
30
- * @property {{ row: number, column: number, visible: boolean }} [cursor]
31
- * @property {string} [title]
32
- */
33
-
34
- const FG_CODES = {
35
- red: 31,
36
- green: 32,
37
- yellow: 33,
38
- blue: 34,
39
- cyan: 36,
40
- white: 37,
41
- };
42
-
43
- const BG_CODES = {
44
- blue: 44,
45
- cyan: 46,
46
- bgRed: 41,
47
- bgGreen: 42,
48
- };
49
-
50
- /**
51
- * Strip or sanitize untrusted control characters and ANSI escape sequences.
52
- * @param {string} text
53
- * @returns {string}
54
- */
55
- export function sanitizeControlChars(text) {
56
- if (text === null || text === undefined) return "";
57
- // Strip CSI, OSC, and single-byte ANSI sequences before removing controls.
58
- return stripAnsi(text).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
59
- }
60
-
61
- /**
62
- * Encode style to ANSI escape code sequence.
63
- * @param {TextStyle} [style]
64
- * @returns {string}
65
- */
66
- export function encodeAnsiStyle(style) {
67
- if (!style) return "";
68
- const codes = [];
69
-
70
- if (style.bold) codes.push(1);
71
- if (style.dim) codes.push(2);
72
- if (style.underline) codes.push(4);
73
- if (style.inverse) codes.push(7);
74
-
75
- if (style.fg && FG_CODES[style.fg]) {
76
- codes.push(FG_CODES[style.fg]);
77
- }
78
- if (style.bg && BG_CODES[style.bg]) {
79
- codes.push(BG_CODES[style.bg]);
80
- }
81
-
82
- if (codes.length === 0) return "";
83
- return `\u001b[${codes.join(";")}m`;
84
- }
85
-
86
- /**
87
- * Format cell runs into line string with ANSI styling applied.
88
- * @param {ScreenLine} line
89
- * @param {number} maxWidth
90
- * @param {import("./capabilities.mjs").TerminalCapabilities} capabilities
91
- * @returns {string}
92
- */
93
- export function renderLineToAnsi(line, maxWidth, capabilities) {
94
- let lineText = "";
95
- let currentWidth = 0;
96
-
97
- for (const run of line.runs || []) {
98
- if (currentWidth >= maxWidth) break;
99
- const cleanText = sanitizeControlChars(run.text);
100
- const runWidth = getStringWidth(cleanText);
101
-
102
- let textToDraw = cleanText;
103
- if (currentWidth + runWidth > maxWidth) {
104
- textToDraw = clipText(cleanText, maxWidth - currentWidth, "");
105
- }
106
-
107
- const runDrawWidth = getStringWidth(textToDraw);
108
- currentWidth += runDrawWidth;
109
-
110
- if (capabilities.ansi && run.style) {
111
- const styleAnsi = encodeAnsiStyle(run.style);
112
- if (styleAnsi) {
113
- lineText += `${styleAnsi}${textToDraw}\u001b[0m`;
114
- } else {
115
- lineText += textToDraw;
116
- }
117
- } else {
118
- lineText += textToDraw;
119
- }
120
- }
121
-
122
- // Pad remaining line width with spaces
123
- if (currentWidth < maxWidth) {
124
- lineText += " ".repeat(maxWidth - currentWidth);
125
- }
126
-
127
- return lineText;
128
- }
129
-
130
- /**
131
- * Render complete frame into ANSI string buffer.
132
- * @param {RenderFrame} frame
133
- * @param {import("./capabilities.mjs").TerminalCapabilities} capabilities
134
- * @returns {string}
135
- */
136
- export function renderFrameToAnsi(frame, capabilities) {
137
- if (!capabilities.ansi) {
138
- return renderFrameToString(frame);
139
- }
140
-
141
- let output = capabilities.alternateScreen ? "\u001b[H" : "\u001b[1;1H";
142
-
143
- const renderLines = frame.lines.slice(0, frame.height);
144
- for (let i = 0; i < frame.height; i++) {
145
- const line = renderLines[i] || { runs: [] };
146
- const lineAnsi = renderLineToAnsi(line, frame.width, capabilities);
147
- output += lineAnsi;
148
- if (i < frame.height - 1) {
149
- output += "\n";
150
- }
151
- }
152
-
153
- if (frame.cursor && frame.cursor.visible) {
154
- const row = Math.min(frame.height, Math.max(1, frame.cursor.row));
155
- const col = Math.min(frame.width, Math.max(1, frame.cursor.column));
156
- output += `\u001b[${row};${col}H\u001b[?25h`;
157
- } else {
158
- output += "\u001b[?25l";
159
- }
160
-
161
- return output;
162
- }
163
-
164
- /**
165
- * Render frame into plain string lines for non-TTY or testing assertions.
166
- * @param {RenderFrame} frame
167
- * @returns {string}
168
- */
169
- export function renderFrameToString(frame) {
170
- const lines = [];
171
- for (let i = 0; i < frame.height; i++) {
172
- const line = frame.lines[i] || { runs: [] };
173
- let lineStr = "";
174
- let width = 0;
175
- for (const run of line.runs || []) {
176
- const clean = sanitizeControlChars(run.text);
177
- lineStr += clean;
178
- width += getStringWidth(clean);
179
- }
180
- if (width < frame.width) {
181
- lineStr += " ".repeat(frame.width - width);
182
- } else if (width > frame.width) {
183
- lineStr = clipText(lineStr, frame.width, "");
184
- }
185
- lines.push(lineStr);
186
- }
187
- return lines.join("\n");
188
- }