@proletariat/cli 0.3.35 → 0.3.40
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.
- package/README.md +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/agent/auth.d.ts +12 -2
- package/dist/commands/agent/auth.js +128 -4
- package/dist/commands/agent/list.js +16 -7
- package/dist/commands/agent/status.js +32 -4
- package/dist/commands/board/watch.js +6 -0
- package/dist/commands/branch/list.d.ts +1 -0
- package/dist/commands/branch/list.js +43 -12
- package/dist/commands/branch/where.js +9 -19
- package/dist/commands/category/list.d.ts +2 -1
- package/dist/commands/category/list.js +38 -13
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/{claude.js → claude/index.js} +12 -12
- package/dist/commands/claude/open.d.ts +13 -0
- package/dist/commands/claude/open.js +175 -0
- package/dist/commands/diet.js +18 -2
- package/dist/commands/docker/logs.js +7 -3
- package/dist/commands/docker/shell.js +6 -0
- package/dist/commands/docker/start.js +20 -4
- package/dist/commands/docker/sync.d.ts +4 -0
- package/dist/commands/docker/sync.js +30 -2
- package/dist/commands/epic/show.d.ts +13 -0
- package/dist/commands/epic/show.js +16 -0
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/epic/view.js +27 -0
- package/dist/commands/execution/config.d.ts +0 -4
- package/dist/commands/execution/config.js +14 -46
- package/dist/commands/execution/index.js +2 -1
- package/dist/commands/execution/logs.js +7 -1
- package/dist/commands/execution/stop.js +2 -1
- package/dist/commands/execution/view.js +30 -26
- package/dist/commands/init.js +2 -19
- package/dist/commands/label/create.js +2 -1
- package/dist/commands/label/delete.js +2 -1
- package/dist/commands/label/group/create.js +2 -1
- package/dist/commands/label/group/list.js +2 -1
- package/dist/commands/label/list.js +2 -1
- package/dist/commands/mcp-server.js +27 -1
- package/dist/commands/phase/template/list.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/project/create.js +3 -4
- package/dist/commands/project/update.js +5 -6
- package/dist/commands/pull.js +24 -0
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/create.d.ts +19 -0
- package/dist/commands/session/create.js +102 -0
- package/dist/commands/session/health.js +4 -23
- package/dist/commands/session/index.js +14 -1
- package/dist/commands/session/list.js +9 -8
- package/dist/commands/session/peek.d.ts +38 -0
- package/dist/commands/session/peek.js +316 -0
- package/dist/commands/session/poke.d.ts +27 -0
- package/dist/commands/session/poke.js +219 -0
- package/dist/commands/spec/view.js +29 -0
- package/dist/commands/template/list.js +2 -1
- package/dist/commands/theme/add-names.d.ts +4 -0
- package/dist/commands/theme/add-names.js +11 -1
- package/dist/commands/theme/create.d.ts +2 -0
- package/dist/commands/theme/create.js +8 -0
- package/dist/commands/ticket/bulk.js +2 -2
- package/dist/commands/ticket/complete.js +2 -2
- package/dist/commands/ticket/create.js +21 -0
- package/dist/commands/ticket/delete.js +8 -0
- package/dist/commands/ticket/edit.js +25 -0
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/ticket/index.js +2 -2
- package/dist/commands/ticket/move.js +25 -2
- package/dist/commands/ticket/resolve.js +3 -4
- package/dist/commands/ticket/show.d.ts +13 -0
- package/dist/commands/ticket/show.js +16 -0
- package/dist/commands/ticket/template/list.js +2 -1
- package/dist/commands/ticket/view.d.ts +0 -1
- package/dist/commands/ticket/view.js +30 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +186 -103
- package/dist/commands/work/status.d.ts +14 -0
- package/dist/commands/work/status.js +60 -0
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workflow/index.js +2 -1
- package/dist/commands/workflow/show.d.ts +13 -0
- package/dist/commands/workflow/show.js +16 -0
- package/dist/commands/workspace/add.js +15 -0
- package/dist/commands/workspace/list.js +2 -1
- package/dist/commands/workspace/prune.js +7 -7
- package/dist/hooks/init.js +10 -2
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/branch/index.d.ts +1 -0
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/config.d.ts +15 -1
- package/dist/lib/execution/config.js +28 -0
- package/dist/lib/execution/runners.d.ts +45 -0
- package/dist/lib/execution/runners.js +187 -26
- package/dist/lib/execution/session-utils.d.ts +16 -1
- package/dist/lib/execution/session-utils.js +71 -4
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +6 -1
- package/dist/lib/execution/storage.js +35 -5
- package/dist/lib/execution/types.d.ts +3 -0
- package/dist/lib/mcp/tools/board.js +4 -6
- package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
- package/dist/lib/mcp/tools/epic.js +8 -3
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/spec.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +11 -9
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +148 -6
- package/dist/lib/mcp/types.d.ts +10 -0
- package/dist/lib/multiline-input.js +2 -1
- package/dist/lib/pmo/base-command.js +4 -4
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/actions.js +1 -1
- package/dist/lib/pmo/storage/base.js +402 -50
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/types.d.ts +1 -0
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-command.d.ts +20 -0
- package/dist/lib/prompt-command.js +38 -2
- package/dist/lib/prompt-json.d.ts +41 -4
- package/dist/lib/prompt-json.js +138 -7
- package/dist/lib/styles.d.ts +37 -0
- package/dist/lib/styles.js +73 -0
- package/oclif.manifest.json +4046 -3385
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -2,9 +2,34 @@
|
|
|
2
2
|
* Session Utilities
|
|
3
3
|
*
|
|
4
4
|
* Shared utilities for tmux session naming, parsing, and discovery.
|
|
5
|
-
* Used by session/list.ts and session/
|
|
5
|
+
* Used by session/list.ts, session/attach.ts, session/health.ts, and session/peek.ts commands.
|
|
6
6
|
*/
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Capture the last N lines from a tmux pane.
|
|
10
|
+
* Supports both host tmux sessions and container tmux sessions (via docker exec).
|
|
11
|
+
*
|
|
12
|
+
* @param sessionId - The tmux session ID to capture from
|
|
13
|
+
* @param lines - Number of scrollback lines to capture
|
|
14
|
+
* @param containerId - Optional container ID for container-based sessions
|
|
15
|
+
* @returns The captured pane content, or null if capture fails
|
|
16
|
+
*/
|
|
17
|
+
export function captureTmuxPane(sessionId, lines, containerId) {
|
|
18
|
+
try {
|
|
19
|
+
const captureCmd = `tmux capture-pane -t "${sessionId}" -p -S -${lines}`;
|
|
20
|
+
if (containerId) {
|
|
21
|
+
return execSync(`docker exec ${containerId} bash -c '${captureCmd}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 }).trim();
|
|
22
|
+
}
|
|
23
|
+
return execSync(captureCmd, {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
8
33
|
/**
|
|
9
34
|
* Known action names used in session naming.
|
|
10
35
|
* These are the actions defined in pmo/actions/ that may be used when spawning agents.
|
|
@@ -102,10 +127,37 @@ export function getHostTmuxSessionNames() {
|
|
|
102
127
|
export function getContainerTmuxSessionMap() {
|
|
103
128
|
const sessionMap = new Map();
|
|
104
129
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
130
|
+
const containerIds = new Set();
|
|
131
|
+
// Primary: devcontainer-labeled containers (historical behavior).
|
|
132
|
+
try {
|
|
133
|
+
const labeledOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
134
|
+
if (labeledOutput) {
|
|
135
|
+
for (const id of labeledOutput.split('\n')) {
|
|
136
|
+
if (id.trim())
|
|
137
|
+
containerIds.add(id.trim());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore and continue with broad fallback discovery.
|
|
143
|
+
}
|
|
144
|
+
// Fallback: include all running containers so prlt-managed devcontainers
|
|
145
|
+
// without the devcontainer.local_folder label are still discoverable.
|
|
146
|
+
try {
|
|
147
|
+
const allOutput = execSync('docker ps --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
148
|
+
if (allOutput) {
|
|
149
|
+
for (const id of allOutput.split('\n')) {
|
|
150
|
+
if (id.trim())
|
|
151
|
+
containerIds.add(id.trim());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Docker not available.
|
|
157
|
+
}
|
|
158
|
+
if (containerIds.size === 0)
|
|
107
159
|
return sessionMap;
|
|
108
|
-
for (const containerId of
|
|
160
|
+
for (const containerId of containerIds) {
|
|
109
161
|
try {
|
|
110
162
|
const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
111
163
|
if (tmuxOutput) {
|
|
@@ -134,6 +186,21 @@ export function flattenContainerSessions(containerTmuxSessions) {
|
|
|
134
186
|
});
|
|
135
187
|
return result;
|
|
136
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Find container sessions using prefix matching.
|
|
191
|
+
* Handles short vs full container ID mismatches between DB records and docker output.
|
|
192
|
+
*/
|
|
193
|
+
export function findContainerSessionsByPrefix(containerTmuxSessions, containerId) {
|
|
194
|
+
const exact = containerTmuxSessions.get(containerId);
|
|
195
|
+
if (exact)
|
|
196
|
+
return exact;
|
|
197
|
+
for (const [key, sessions] of containerTmuxSessions) {
|
|
198
|
+
if (key.startsWith(containerId) || containerId.startsWith(key)) {
|
|
199
|
+
return sessions;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
137
204
|
/**
|
|
138
205
|
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
139
206
|
* First tries exact matches with known action names, then falls back to
|
|
@@ -13,7 +13,7 @@ import { findHQRoot } from '../repos/index.js';
|
|
|
13
13
|
import { hasGitHubRemote } from '../repos/git.js';
|
|
14
14
|
import { hasDevcontainerConfig } from './devcontainer.js';
|
|
15
15
|
import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
|
|
16
|
-
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
|
|
16
|
+
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight } from './runners.js';
|
|
17
17
|
import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
|
|
18
18
|
import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
|
|
19
19
|
// =============================================================================
|
|
@@ -282,6 +282,19 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
|
|
|
282
282
|
}
|
|
283
283
|
const displayMode = options.displayMode || 'terminal';
|
|
284
284
|
const sandboxed = !(options.skipPermissions ?? false);
|
|
285
|
+
// Executor preflight check (TKT-1082): verify binary is available before proceeding
|
|
286
|
+
// For host environment, check immediately. For devcontainer, check happens after container start.
|
|
287
|
+
if (environment === 'host') {
|
|
288
|
+
const preflight = runExecutorPreflight(executor, environment);
|
|
289
|
+
if (!preflight.ok) {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
ticketId: ticket.id,
|
|
293
|
+
agentName,
|
|
294
|
+
error: preflight.error,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
285
298
|
// Create branch in worktree(s)
|
|
286
299
|
// For devcontainer environments, run git commands inside the container
|
|
287
300
|
// because the worktree .git file has container paths, not host paths
|
|
@@ -438,7 +451,7 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
|
|
|
438
451
|
};
|
|
439
452
|
}
|
|
440
453
|
else {
|
|
441
|
-
executionStorage.updateStatus(execution.id, 'failed');
|
|
454
|
+
executionStorage.updateStatus(execution.id, 'failed', undefined, result.error);
|
|
442
455
|
return {
|
|
443
456
|
success: false,
|
|
444
457
|
executionId: execution.id,
|
|
@@ -33,7 +33,7 @@ export declare class ExecutionStorage {
|
|
|
33
33
|
/**
|
|
34
34
|
* Update execution status
|
|
35
35
|
*/
|
|
36
|
-
updateStatus(id: string, status: ExecutionStatus, exitCode?: number): void;
|
|
36
|
+
updateStatus(id: string, status: ExecutionStatus, exitCode?: number, errorMessage?: string): void;
|
|
37
37
|
/**
|
|
38
38
|
* Update execution with process info
|
|
39
39
|
*/
|
|
@@ -71,6 +71,11 @@ export declare class ExecutionStorage {
|
|
|
71
71
|
* Returns the number of stale executions cleaned up.
|
|
72
72
|
*/
|
|
73
73
|
cleanupStaleExecutions(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Find container sessions using prefix matching.
|
|
76
|
+
* Handles cases where the stored containerId format differs from docker ps output.
|
|
77
|
+
*/
|
|
78
|
+
private findContainerSessionsByPrefix;
|
|
74
79
|
/**
|
|
75
80
|
* Get list of host tmux session names
|
|
76
81
|
*/
|
|
@@ -29,6 +29,7 @@ function rowToAgentWork(row) {
|
|
|
29
29
|
startedAt: new Date(row.started_at),
|
|
30
30
|
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
31
31
|
exitCode: row.exit_code ?? undefined,
|
|
32
|
+
errorMessage: row.error_message || undefined,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
// =============================================================================
|
|
@@ -68,14 +69,28 @@ export class ExecutionStorage {
|
|
|
68
69
|
/**
|
|
69
70
|
* Update execution status
|
|
70
71
|
*/
|
|
71
|
-
updateStatus(id, status, exitCode) {
|
|
72
|
+
updateStatus(id, status, exitCode, errorMessage) {
|
|
72
73
|
const completedAt = ['completed', 'failed', 'stopped'].includes(status) ? Date.now() : null;
|
|
73
|
-
if (exitCode !== undefined) {
|
|
74
|
+
if (exitCode !== undefined && errorMessage) {
|
|
75
|
+
this.db.prepare(`
|
|
76
|
+
UPDATE ${T.agent_work}
|
|
77
|
+
SET status = ?, completed_at = ?, exit_code = ?, error_message = ?
|
|
78
|
+
WHERE id = ?
|
|
79
|
+
`).run(status, completedAt, exitCode, errorMessage, id);
|
|
80
|
+
}
|
|
81
|
+
else if (exitCode !== undefined) {
|
|
74
82
|
this.db.prepare(`
|
|
75
83
|
UPDATE ${T.agent_work}
|
|
76
84
|
SET status = ?, completed_at = ?, exit_code = ?
|
|
77
85
|
WHERE id = ?
|
|
78
86
|
`).run(status, completedAt, exitCode, id);
|
|
87
|
+
}
|
|
88
|
+
else if (errorMessage) {
|
|
89
|
+
this.db.prepare(`
|
|
90
|
+
UPDATE ${T.agent_work}
|
|
91
|
+
SET status = ?, completed_at = ?, error_message = ?
|
|
92
|
+
WHERE id = ?
|
|
93
|
+
`).run(status, completedAt, errorMessage, id);
|
|
79
94
|
}
|
|
80
95
|
else {
|
|
81
96
|
this.db.prepare(`
|
|
@@ -215,9 +230,9 @@ export class ExecutionStorage {
|
|
|
215
230
|
}
|
|
216
231
|
let sessionExists = false;
|
|
217
232
|
if (exec.environment === 'devcontainer' && exec.containerId) {
|
|
218
|
-
// Check if session exists in container
|
|
219
|
-
const containerSessions =
|
|
220
|
-
sessionExists = containerSessions
|
|
233
|
+
// Check if session exists in container (use prefix matching for ID format differences)
|
|
234
|
+
const containerSessions = this.findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
|
|
235
|
+
sessionExists = containerSessions.includes(exec.sessionId);
|
|
221
236
|
}
|
|
222
237
|
else {
|
|
223
238
|
// Check if session exists on host
|
|
@@ -231,6 +246,21 @@ export class ExecutionStorage {
|
|
|
231
246
|
}
|
|
232
247
|
return cleanedCount;
|
|
233
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Find container sessions using prefix matching.
|
|
251
|
+
* Handles cases where the stored containerId format differs from docker ps output.
|
|
252
|
+
*/
|
|
253
|
+
findContainerSessionsByPrefix(containerTmuxSessions, containerId) {
|
|
254
|
+
const exact = containerTmuxSessions.get(containerId);
|
|
255
|
+
if (exact)
|
|
256
|
+
return exact;
|
|
257
|
+
for (const [key, sessions] of containerTmuxSessions) {
|
|
258
|
+
if (key.startsWith(containerId) || containerId.startsWith(key)) {
|
|
259
|
+
return sessions;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
234
264
|
/**
|
|
235
265
|
* Get list of host tmux session names
|
|
236
266
|
*/
|
|
@@ -57,6 +57,7 @@ export interface AgentWork {
|
|
|
57
57
|
startedAt: Date;
|
|
58
58
|
completedAt?: Date;
|
|
59
59
|
exitCode?: number;
|
|
60
|
+
errorMessage?: string;
|
|
60
61
|
}
|
|
61
62
|
export interface ExecutionContext {
|
|
62
63
|
ticketId: string;
|
|
@@ -112,6 +113,7 @@ export declare function getBranchType(category?: string): string;
|
|
|
112
113
|
* - agent: the AI agent doing the work
|
|
113
114
|
*/
|
|
114
115
|
export declare function generateBranchName(ticketId: string, ticketTitle: string, ownerName: string, agentName: string, category?: string): string;
|
|
116
|
+
export type AuthMethod = 'oauth' | 'apikey';
|
|
115
117
|
export interface ExecutionConfig {
|
|
116
118
|
defaultEnvironment: ExecutionEnvironment;
|
|
117
119
|
defaultExecutor: ExecutorType;
|
|
@@ -119,6 +121,7 @@ export interface ExecutionConfig {
|
|
|
119
121
|
shell: Shell;
|
|
120
122
|
outputMode: OutputMode;
|
|
121
123
|
sandboxed: boolean;
|
|
124
|
+
authMethod?: AuthMethod;
|
|
122
125
|
tmux: {
|
|
123
126
|
session: string;
|
|
124
127
|
layout: 'split' | 'window';
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* MCP Board Tools
|
|
3
3
|
*/
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { errorResponse, strictTool } from '../helpers.js';
|
|
5
|
+
import { formatTicket, errorResponse, strictTool } from '../helpers.js';
|
|
6
6
|
export function registerBoardTools(server, ctx) {
|
|
7
|
-
strictTool(server, '
|
|
7
|
+
strictTool(server, 'board_view', 'Show the kanban board', { project: z.string().optional().describe('Project ID') }, async (params) => {
|
|
8
8
|
try {
|
|
9
9
|
let projectId = params.project;
|
|
10
10
|
if (!projectId) {
|
|
@@ -27,10 +27,8 @@ export function registerBoardTools(server, ctx) {
|
|
|
27
27
|
position: col.position,
|
|
28
28
|
ticketCount: col.tickets.length,
|
|
29
29
|
tickets: col.tickets.map((t) => ({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
priority: t.priority,
|
|
33
|
-
assignee: t.assignee,
|
|
30
|
+
...formatTicket(t),
|
|
31
|
+
labels: t.labels,
|
|
34
32
|
})),
|
|
35
33
|
})),
|
|
36
34
|
updatedAt: board.updatedAt.toISOString(),
|
|
@@ -17,9 +17,26 @@ export function registerAgentTools(server, ctx) {
|
|
|
17
17
|
return errorResponse(error);
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
|
-
strictTool(server, 'agent_status', 'Check agent status', {}, async () => {
|
|
20
|
+
strictTool(server, 'agent_status', 'Check agent status. With agent param, returns that agent\'s status. Without, auto-detects current agent or returns all agent statuses.', { agent: z.string().optional().describe('Agent name. If omitted, auto-detects current agent or returns all.') }, async (params) => {
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
if (params.agent) {
|
|
23
|
+
const output = ctx.runCommand(`prlt agent status ${params.agent} --json`);
|
|
24
|
+
return textResponse(output);
|
|
25
|
+
}
|
|
26
|
+
// Try auto-detecting current agent via whoami
|
|
27
|
+
try {
|
|
28
|
+
const whoamiOutput = ctx.runCommand('prlt whoami --json');
|
|
29
|
+
const whoami = JSON.parse(whoamiOutput);
|
|
30
|
+
if (whoami.agent) {
|
|
31
|
+
const output = ctx.runCommand(`prlt agent status ${whoami.agent} --json`);
|
|
32
|
+
return textResponse(output);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// whoami failed or no agent detected, fall through
|
|
37
|
+
}
|
|
38
|
+
// No agent detected - return all agent statuses
|
|
39
|
+
const output = ctx.runCommand('prlt agent status --json');
|
|
23
40
|
return textResponse(output);
|
|
24
41
|
}
|
|
25
42
|
catch (error) {
|
|
@@ -139,7 +156,7 @@ export function registerRepoTools(server, ctx) {
|
|
|
139
156
|
});
|
|
140
157
|
}
|
|
141
158
|
export function registerBranchTools(server, ctx) {
|
|
142
|
-
strictTool(server, 'branch_list', 'List branches', {}, async () => {
|
|
159
|
+
strictTool(server, 'branch_list', 'List branches (across all HQ repos if not in a git repo)', {}, async () => {
|
|
143
160
|
try {
|
|
144
161
|
const output = ctx.runCommand('prlt branch list');
|
|
145
162
|
return textResponse(output);
|
|
@@ -161,9 +178,11 @@ export function registerBranchTools(server, ctx) {
|
|
|
161
178
|
return errorResponse(error);
|
|
162
179
|
}
|
|
163
180
|
});
|
|
164
|
-
strictTool(server, 'branch_where', '
|
|
181
|
+
strictTool(server, 'branch_where', 'Find which directory a branch is checked out in', {
|
|
182
|
+
search: z.string().describe('Branch name or ticket ID to search for'),
|
|
183
|
+
}, async (params) => {
|
|
165
184
|
try {
|
|
166
|
-
const output = ctx.runCommand(
|
|
185
|
+
const output = ctx.runCommand(`prlt branch where ${params.search}`);
|
|
167
186
|
return textResponse(output);
|
|
168
187
|
}
|
|
169
188
|
catch (error) {
|
|
@@ -314,7 +333,7 @@ export function registerUtilityTools(server, ctx) {
|
|
|
314
333
|
});
|
|
315
334
|
strictTool(server, 'session_list', 'List tmux sessions', {}, async () => {
|
|
316
335
|
try {
|
|
317
|
-
const output = ctx.runCommand('prlt session list');
|
|
336
|
+
const output = ctx.runCommand('prlt session list --json');
|
|
318
337
|
return textResponse(output);
|
|
319
338
|
}
|
|
320
339
|
catch (error) {
|
|
@@ -71,7 +71,7 @@ export function registerEpicTools(server, ctx) {
|
|
|
71
71
|
return errorResponse(error);
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
strictTool(server, '
|
|
74
|
+
strictTool(server, 'epic_view', 'Get epic details with tickets', { id: z.string().describe('Epic ID') }, async (params) => {
|
|
75
75
|
try {
|
|
76
76
|
const epic = await ctx.storage.getEpic(params.id);
|
|
77
77
|
if (!epic)
|
|
@@ -83,9 +83,14 @@ export function registerEpicTools(server, ctx) {
|
|
|
83
83
|
text: JSON.stringify({
|
|
84
84
|
success: true,
|
|
85
85
|
epic: {
|
|
86
|
-
|
|
86
|
+
id: epic.id,
|
|
87
|
+
projectId: epic.projectId,
|
|
88
|
+
title: epic.title,
|
|
89
|
+
status: epic.status,
|
|
90
|
+
position: epic.position,
|
|
91
|
+
specId: epic.specId,
|
|
87
92
|
ticketCount: tickets.length,
|
|
88
|
-
tickets: tickets.map((t) => ({ id: t.id, title: t.title, statusName: t.statusName, priority: t.priority })),
|
|
93
|
+
tickets: tickets.map((t) => ({ id: t.id, title: t.title, statusName: t.statusName, priority: t.priority, category: t.category })),
|
|
89
94
|
createdAt: epic.createdAt.toISOString(),
|
|
90
95
|
updatedAt: epic.updatedAt.toISOString(),
|
|
91
96
|
},
|
|
@@ -17,4 +17,5 @@ export { registerTemplateTools } from './template.js';
|
|
|
17
17
|
export { registerViewTools } from './view.js';
|
|
18
18
|
export { registerDietTools } from './diet.js';
|
|
19
19
|
export { registerLabelTools } from './label.js';
|
|
20
|
+
export { registerTmuxTools } from './tmux.js';
|
|
20
21
|
export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
|
|
@@ -17,5 +17,6 @@ export { registerTemplateTools } from './template.js';
|
|
|
17
17
|
export { registerViewTools } from './view.js';
|
|
18
18
|
export { registerDietTools } from './diet.js';
|
|
19
19
|
export { registerLabelTools } from './label.js';
|
|
20
|
+
export { registerTmuxTools } from './tmux.js';
|
|
20
21
|
// CLI passthrough tools
|
|
21
22
|
export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
|
|
@@ -67,7 +67,7 @@ export function registerSpecTools(server, ctx) {
|
|
|
67
67
|
return errorResponse(error);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
|
-
strictTool(server, '
|
|
70
|
+
strictTool(server, 'spec_view', 'Get spec details', { id: z.string().describe('Spec ID') }, async (params) => {
|
|
71
71
|
try {
|
|
72
72
|
const spec = await ctx.storage.getSpec(params.id);
|
|
73
73
|
if (!spec)
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { formatTicket, formatTicketFull, errorResponse, strictTool } from '../helpers.js';
|
|
6
6
|
import { getWorkspacePriorities, setWorkspacePriorities } from '../../pmo/utils.js';
|
|
7
7
|
export function registerTicketTools(server, ctx) {
|
|
8
|
-
strictTool(server, 'ticket_list', 'List tickets with optional filters', {
|
|
8
|
+
strictTool(server, 'ticket_list', 'List tickets with optional filters. Returns summary fields only (no descriptions). Use ticket_show for full details.', {
|
|
9
9
|
project: z.string().optional().describe('Project ID'),
|
|
10
10
|
column: z.string().optional().describe('Filter by column/status'),
|
|
11
11
|
priority: z.string().optional().describe('Filter by priority (uses workspace priority scale)'),
|
|
@@ -17,9 +17,11 @@ export function registerTicketTools(server, ctx) {
|
|
|
17
17
|
label: z.string().optional().describe('Filter by label name'),
|
|
18
18
|
label_group: z.string().optional().describe('Filter by label group name'),
|
|
19
19
|
all_projects: z.boolean().optional().describe('List from all projects'),
|
|
20
|
+
limit: z.number().min(1).optional().describe('Maximum number of tickets to return (default: 50)'),
|
|
21
|
+
offset: z.number().min(0).optional().describe('Number of tickets to skip for pagination (default: 0)'),
|
|
20
22
|
}, async (params) => {
|
|
21
23
|
try {
|
|
22
|
-
const
|
|
24
|
+
const allTickets = await ctx.storage.listTickets(params.all_projects ? undefined : params.project, {
|
|
23
25
|
column: params.column,
|
|
24
26
|
priority: params.priority,
|
|
25
27
|
category: params.category,
|
|
@@ -31,31 +33,31 @@ export function registerTicketTools(server, ctx) {
|
|
|
31
33
|
labelGroup: params.label_group,
|
|
32
34
|
allProjects: params.all_projects,
|
|
33
35
|
});
|
|
36
|
+
const total = allTickets.length;
|
|
37
|
+
const offset = params.offset ?? 0;
|
|
38
|
+
const limit = params.limit ?? 50;
|
|
39
|
+
const tickets = allTickets.slice(offset, offset + limit);
|
|
34
40
|
return {
|
|
35
41
|
content: [{
|
|
36
42
|
type: 'text',
|
|
37
43
|
text: JSON.stringify({
|
|
38
44
|
success: true,
|
|
45
|
+
total,
|
|
39
46
|
count: tickets.length,
|
|
47
|
+
offset,
|
|
48
|
+
limit,
|
|
40
49
|
tickets: await Promise.all(tickets.map(async (t) => {
|
|
41
50
|
const ticketLabels = await ctx.storage.getLabelsForTicket(t.id);
|
|
42
51
|
return {
|
|
43
52
|
id: t.id,
|
|
44
53
|
title: t.title,
|
|
45
|
-
description: t.description,
|
|
46
54
|
priority: t.priority,
|
|
47
55
|
category: t.category,
|
|
48
56
|
statusName: t.statusName,
|
|
49
57
|
statusCategory: t.statusCategory,
|
|
50
|
-
projectId: t.projectId,
|
|
51
58
|
assignee: t.assignee,
|
|
52
|
-
owner: t.owner,
|
|
53
|
-
epicId: t.epicId,
|
|
54
|
-
branch: t.branch,
|
|
55
59
|
position: t.position,
|
|
56
60
|
labels: ticketLabels.map(l => ({ id: l.id, name: l.name, groupName: l.groupName })),
|
|
57
|
-
createdAt: t.createdAt.toISOString(),
|
|
58
|
-
updatedAt: t.updatedAt.toISOString(),
|
|
59
61
|
};
|
|
60
62
|
})),
|
|
61
63
|
}, null, 2),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tmux Tools
|
|
3
|
+
*
|
|
4
|
+
* Provides tmux interaction tools for AI agents to drive interactive CLI sessions.
|
|
5
|
+
* Used by the explore-cli action to perform exploratory QA testing.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - tmux_send_keys: Send keystrokes to a tmux session
|
|
9
|
+
* - tmux_capture_pane: Capture current terminal screen from a tmux session
|
|
10
|
+
* - tmux_start_session: Start a new tmux session with an optional command
|
|
11
|
+
* - tmux_list_sessions: List active tmux sessions
|
|
12
|
+
* - tmux_kill_session: Kill a tmux session
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import type { McpToolContext } from '../types.js';
|
|
16
|
+
export declare function registerTmuxTools(server: McpServer, _ctx: McpToolContext): void;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tmux Tools
|
|
3
|
+
*
|
|
4
|
+
* Provides tmux interaction tools for AI agents to drive interactive CLI sessions.
|
|
5
|
+
* Used by the explore-cli action to perform exploratory QA testing.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - tmux_send_keys: Send keystrokes to a tmux session
|
|
9
|
+
* - tmux_capture_pane: Capture current terminal screen from a tmux session
|
|
10
|
+
* - tmux_start_session: Start a new tmux session with an optional command
|
|
11
|
+
* - tmux_list_sessions: List active tmux sessions
|
|
12
|
+
* - tmux_kill_session: Kill a tmux session
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { errorResponse, strictTool, successResponse } from '../helpers.js';
|
|
17
|
+
const TMUX_ENV = { ...process.env, TERM: process.env.TERM || 'xterm-256color' };
|
|
18
|
+
/**
|
|
19
|
+
* Execute a tmux command with args passed as an array (no shell interpretation).
|
|
20
|
+
*/
|
|
21
|
+
function runTmux(args, timeoutMs = 10_000) {
|
|
22
|
+
return execFileSync('tmux', args, {
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
timeout: timeoutMs,
|
|
25
|
+
env: TMUX_ENV,
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
/** Validate session name — alphanumeric, hyphens, underscores, dots only. */
|
|
29
|
+
function validateSessionName(name) {
|
|
30
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
|
|
31
|
+
throw new Error(`Invalid session name: "${name}". Only alphanumeric, hyphens, underscores, and dots allowed.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a tmux session exists.
|
|
36
|
+
*/
|
|
37
|
+
function sessionExists(sessionName) {
|
|
38
|
+
try {
|
|
39
|
+
runTmux(['has-session', '-t', sessionName]);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function registerTmuxTools(server, _ctx) {
|
|
47
|
+
// ── tmux_send_keys ───────────────────────────────────────────────────
|
|
48
|
+
strictTool(server, 'tmux_send_keys', 'Send keystrokes to a tmux session. Supports text input and special keys like Enter, Escape, Up, Down, Left, Right, C-c (Ctrl+C), Tab, BSpace, etc. Use this to interact with interactive CLI menus and prompts.', {
|
|
49
|
+
session: z.string().describe('Tmux session name to send keys to'),
|
|
50
|
+
keys: z.string().describe('Keys to send. Text is sent literally; special keys: Enter, Escape, Up, Down, Left, Right, C-c, C-d, Tab, BSpace, Space, Home, End, PPage, NPage'),
|
|
51
|
+
literal: z.boolean().optional().describe('If true, send keys as literal text (disables special key lookup). Default false.'),
|
|
52
|
+
delay_ms: z.number().optional().describe('Milliseconds to wait after sending keys before returning (default 100). Useful to let the UI render.'),
|
|
53
|
+
}, async (params) => {
|
|
54
|
+
try {
|
|
55
|
+
validateSessionName(params.session);
|
|
56
|
+
if (!sessionExists(params.session)) {
|
|
57
|
+
return errorResponse(new Error(`Tmux session not found: ${params.session}`));
|
|
58
|
+
}
|
|
59
|
+
const args = ['send-keys', '-t', params.session];
|
|
60
|
+
if (params.literal)
|
|
61
|
+
args.push('-l');
|
|
62
|
+
args.push(params.keys);
|
|
63
|
+
runTmux(args);
|
|
64
|
+
// Wait for UI to render
|
|
65
|
+
const delay = Math.min(params.delay_ms ?? 100, 30_000);
|
|
66
|
+
if (delay > 0) {
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
68
|
+
}
|
|
69
|
+
return successResponse({ sent: params.keys, session: params.session });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return errorResponse(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// ── tmux_capture_pane ────────────────────────────────────────────────
|
|
76
|
+
strictTool(server, 'tmux_capture_pane', 'Capture the current visible content of a tmux pane. Returns the terminal screen text, useful for reading CLI output, menu states, error messages, and interactive prompt displays.', {
|
|
77
|
+
session: z.string().describe('Tmux session name to capture from'),
|
|
78
|
+
start_line: z.number().optional().describe('Start line for capture (negative = scrollback). Default: beginning of visible pane.'),
|
|
79
|
+
end_line: z.number().optional().describe('End line for capture. Default: end of visible pane.'),
|
|
80
|
+
escape_sequences: z.boolean().optional().describe('If true, include ANSI escape sequences in output. Default false (plain text).'),
|
|
81
|
+
}, async (params) => {
|
|
82
|
+
try {
|
|
83
|
+
validateSessionName(params.session);
|
|
84
|
+
if (!sessionExists(params.session)) {
|
|
85
|
+
return errorResponse(new Error(`Tmux session not found: ${params.session}`));
|
|
86
|
+
}
|
|
87
|
+
const args = ['capture-pane', '-t', params.session, '-p'];
|
|
88
|
+
if (params.escape_sequences)
|
|
89
|
+
args.push('-e');
|
|
90
|
+
if (params.start_line !== undefined)
|
|
91
|
+
args.push('-S', String(params.start_line));
|
|
92
|
+
if (params.end_line !== undefined)
|
|
93
|
+
args.push('-E', String(params.end_line));
|
|
94
|
+
const content = runTmux(args);
|
|
95
|
+
return successResponse({
|
|
96
|
+
session: params.session,
|
|
97
|
+
content,
|
|
98
|
+
lines: content.split('\n').length,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return errorResponse(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// ── tmux_start_session ───────────────────────────────────────────────
|
|
106
|
+
strictTool(server, 'tmux_start_session', 'Start a new tmux session, optionally running a command inside it. Use this to launch a CLI session for exploratory testing.', {
|
|
107
|
+
session: z.string().describe('Name for the new tmux session'),
|
|
108
|
+
command: z.string().optional().describe('Command to run in the session (e.g., "prlt" to launch the CLI)'),
|
|
109
|
+
width: z.number().optional().describe('Terminal width in columns (default 120)'),
|
|
110
|
+
height: z.number().optional().describe('Terminal height in rows (default 40)'),
|
|
111
|
+
}, async (params) => {
|
|
112
|
+
try {
|
|
113
|
+
validateSessionName(params.session);
|
|
114
|
+
if (sessionExists(params.session)) {
|
|
115
|
+
return errorResponse(new Error(`Tmux session already exists: ${params.session}`));
|
|
116
|
+
}
|
|
117
|
+
const width = params.width ?? 120;
|
|
118
|
+
const height = params.height ?? 40;
|
|
119
|
+
const args = ['new-session', '-d', '-s', params.session, '-x', String(width), '-y', String(height)];
|
|
120
|
+
if (params.command)
|
|
121
|
+
args.push(params.command);
|
|
122
|
+
runTmux(args);
|
|
123
|
+
// Give the session a moment to initialize
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
125
|
+
return successResponse({
|
|
126
|
+
session: params.session,
|
|
127
|
+
width,
|
|
128
|
+
height,
|
|
129
|
+
command: params.command || '(default shell)',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return errorResponse(error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// ── tmux_list_sessions ───────────────────────────────────────────────
|
|
137
|
+
strictTool(server, 'tmux_list_sessions', 'List all active tmux sessions with their status and dimensions.', {}, async () => {
|
|
138
|
+
try {
|
|
139
|
+
let output;
|
|
140
|
+
try {
|
|
141
|
+
output = runTmux(['list-sessions', '-F', '#{session_name}|#{session_width}|#{session_height}|#{session_windows}|#{session_created}']);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// No sessions running
|
|
145
|
+
return successResponse({ sessions: [] });
|
|
146
|
+
}
|
|
147
|
+
const sessions = output.split('\n').filter(Boolean).map((line) => {
|
|
148
|
+
const [name, width, height, windows, created] = line.split('|');
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
width: parseInt(width, 10),
|
|
152
|
+
height: parseInt(height, 10),
|
|
153
|
+
windows: parseInt(windows, 10),
|
|
154
|
+
created: new Date(parseInt(created, 10) * 1000).toISOString(),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
return successResponse({ sessions });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return errorResponse(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// ── tmux_kill_session ────────────────────────────────────────────────
|
|
164
|
+
strictTool(server, 'tmux_kill_session', 'Kill (terminate) a tmux session and all processes running in it.', {
|
|
165
|
+
session: z.string().describe('Name of the tmux session to kill'),
|
|
166
|
+
}, async (params) => {
|
|
167
|
+
try {
|
|
168
|
+
validateSessionName(params.session);
|
|
169
|
+
if (!sessionExists(params.session)) {
|
|
170
|
+
return errorResponse(new Error(`Tmux session not found: ${params.session}`));
|
|
171
|
+
}
|
|
172
|
+
runTmux(['kill-session', '-t', params.session]);
|
|
173
|
+
return successResponse({
|
|
174
|
+
killed: params.session,
|
|
175
|
+
message: `Session "${params.session}" terminated`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return errorResponse(error);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|