@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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { styles } from '../../lib/styles.js';
|
|
5
|
+
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
6
|
+
import { ExecutionStorage } from '../../lib/execution/index.js';
|
|
7
|
+
import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
|
|
8
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
9
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Command
|
|
12
|
+
// =============================================================================
|
|
13
|
+
export default class SessionPeek extends PMOCommand {
|
|
14
|
+
static description = 'View agent tmux pane content without attaching (non-interactive)';
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> session peek altman',
|
|
17
|
+
'<%= config.bin %> session peek TKT-123',
|
|
18
|
+
'<%= config.bin %> session peek WORK-ABCD1234',
|
|
19
|
+
'<%= config.bin %> session peek altman --lines 100',
|
|
20
|
+
'<%= config.bin %> session peek TKT-123 --json',
|
|
21
|
+
'<%= config.bin %> session peek altman | grep error',
|
|
22
|
+
];
|
|
23
|
+
static args = {
|
|
24
|
+
target: Args.string({
|
|
25
|
+
description: 'Agent name, ticket ID (e.g. TKT-123), or execution ID (e.g. WORK-XXXXXXXX)',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
static flags = {
|
|
30
|
+
...pmoBaseFlags,
|
|
31
|
+
lines: Flags.integer({
|
|
32
|
+
char: 'l',
|
|
33
|
+
description: 'Number of scrollback lines to capture',
|
|
34
|
+
default: 50,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
getPMOOptions() {
|
|
38
|
+
return { promptIfMultiple: false };
|
|
39
|
+
}
|
|
40
|
+
async execute() {
|
|
41
|
+
const { args, flags } = await this.parse(SessionPeek);
|
|
42
|
+
const jsonMode = shouldOutputJson(flags);
|
|
43
|
+
// Discover all verified sessions
|
|
44
|
+
const sessions = this.getVerifiedSessions();
|
|
45
|
+
if (sessions.length === 0) {
|
|
46
|
+
if (jsonMode) {
|
|
47
|
+
outputErrorAsJson('NO_SESSIONS', 'No active sessions found.', createMetadata('session peek', flags));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.log('');
|
|
51
|
+
this.log(styles.muted('No active sessions found.'));
|
|
52
|
+
this.log('');
|
|
53
|
+
this.log(styles.muted('Start work with: prlt work start <ticket-id>'));
|
|
54
|
+
this.log('');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (args.target) {
|
|
58
|
+
// Resolve target to matching session(s)
|
|
59
|
+
const matched = this.resolveTarget(args.target, sessions);
|
|
60
|
+
if (matched.length === 0) {
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
outputErrorAsJson('SESSION_NOT_FOUND', `No matching session found for "${args.target}".`, createMetadata('session peek', flags));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.error(`No matching session found for "${args.target}". Run "prlt session list" to see available sessions.`);
|
|
66
|
+
}
|
|
67
|
+
// Output all matched sessions
|
|
68
|
+
if (jsonMode && matched.length > 1) {
|
|
69
|
+
// Collect all captures into a single JSON response
|
|
70
|
+
const results = matched.map(session => {
|
|
71
|
+
const containerId = session.environment === 'container' ? session.containerId : undefined;
|
|
72
|
+
const content = captureTmuxPane(session.sessionId, flags.lines, containerId);
|
|
73
|
+
return {
|
|
74
|
+
sessionId: session.sessionId,
|
|
75
|
+
ticketId: session.ticketId,
|
|
76
|
+
agentName: session.agentName,
|
|
77
|
+
environment: session.environment,
|
|
78
|
+
containerId: session.containerId,
|
|
79
|
+
lines: flags.lines,
|
|
80
|
+
content,
|
|
81
|
+
captureError: content === null
|
|
82
|
+
? `Failed to capture pane content for session "${session.sessionId}".`
|
|
83
|
+
: undefined,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
outputSuccessAsJson({ sessions: results }, createMetadata('session peek', flags));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const session of matched) {
|
|
90
|
+
this.outputPeek(session, flags.lines, jsonMode, flags);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// No target: interactive selection
|
|
95
|
+
const selected = await this.selectFromList({
|
|
96
|
+
message: 'Select a session to peek at:',
|
|
97
|
+
items: sessions,
|
|
98
|
+
getName: (s) => `${s.sessionId} (${s.ticketId}) - ${s.agentName} [${s.environment}]`,
|
|
99
|
+
getValue: (s) => s.sessionId,
|
|
100
|
+
getCommand: (s) => `prlt session peek "${s.sessionId}" --json`,
|
|
101
|
+
jsonMode: jsonMode ? { flags, commandName: 'session peek' } : null,
|
|
102
|
+
});
|
|
103
|
+
if (!selected) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const session = sessions.find(s => s.sessionId === selected);
|
|
107
|
+
if (!session) {
|
|
108
|
+
this.error('No session selected');
|
|
109
|
+
}
|
|
110
|
+
this.outputPeek(session, flags.lines, jsonMode, flags);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a target identifier to matching sessions.
|
|
115
|
+
* Supports: agent name, ticket ID (TKT-XXX), execution ID (WORK-XXX), or session ID.
|
|
116
|
+
*/
|
|
117
|
+
resolveTarget(target, sessions) {
|
|
118
|
+
// Try exact session ID match
|
|
119
|
+
const exactSession = sessions.filter(s => s.sessionId === target);
|
|
120
|
+
if (exactSession.length > 0)
|
|
121
|
+
return exactSession;
|
|
122
|
+
// Try ticket ID match (e.g. TKT-123)
|
|
123
|
+
if (/^[A-Z]+-\d+$/i.test(target)) {
|
|
124
|
+
const ticketTarget = target.toUpperCase();
|
|
125
|
+
const ticketMatches = sessions.filter(s => s.ticketId === ticketTarget);
|
|
126
|
+
if (ticketMatches.length > 0)
|
|
127
|
+
return ticketMatches;
|
|
128
|
+
}
|
|
129
|
+
// Try execution ID match (WORK-XXX) — resolve via DB to find session
|
|
130
|
+
if (target.toUpperCase().startsWith('WORK-')) {
|
|
131
|
+
const executionMatch = this.resolveFromExecution(target.toUpperCase(), sessions);
|
|
132
|
+
if (executionMatch.length > 0)
|
|
133
|
+
return executionMatch;
|
|
134
|
+
}
|
|
135
|
+
// Try agent name match
|
|
136
|
+
const agentMatches = sessions.filter(s => s.agentName === target);
|
|
137
|
+
if (agentMatches.length > 0)
|
|
138
|
+
return agentMatches;
|
|
139
|
+
// Try partial session ID match
|
|
140
|
+
const partialMatches = sessions.filter(s => s.sessionId.includes(target) ||
|
|
141
|
+
s.ticketId.includes(target.toUpperCase()) ||
|
|
142
|
+
s.agentName.includes(target));
|
|
143
|
+
return partialMatches;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve an execution ID (WORK-XXX) to matching sessions.
|
|
147
|
+
*/
|
|
148
|
+
resolveFromExecution(executionId, sessions) {
|
|
149
|
+
let db = null;
|
|
150
|
+
try {
|
|
151
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
152
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
153
|
+
db = new Database(dbPath);
|
|
154
|
+
const executionStorage = new ExecutionStorage(db);
|
|
155
|
+
const execution = executionStorage.getExecution(executionId);
|
|
156
|
+
if (execution) {
|
|
157
|
+
return sessions.filter(s => s.ticketId === execution.ticketId && s.agentName === execution.agentName);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Workspace not available
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
db?.close();
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Output peek content for a session.
|
|
170
|
+
* In raw mode: outputs plain text to stdout.
|
|
171
|
+
* In JSON mode: outputs structured JSON.
|
|
172
|
+
*/
|
|
173
|
+
outputPeek(session, lines, jsonMode, flags) {
|
|
174
|
+
const containerId = session.environment === 'container' ? session.containerId : undefined;
|
|
175
|
+
const content = captureTmuxPane(session.sessionId, lines, containerId);
|
|
176
|
+
if (content === null) {
|
|
177
|
+
if (jsonMode) {
|
|
178
|
+
outputErrorAsJson('CAPTURE_FAILED', `Failed to capture pane content for session "${session.sessionId}". The session may no longer exist or tmux may not be available.`, createMetadata('session peek', flags));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.error(`Failed to capture pane content for session "${session.sessionId}". ` +
|
|
182
|
+
'The session may no longer exist or tmux may not be available.');
|
|
183
|
+
}
|
|
184
|
+
if (jsonMode) {
|
|
185
|
+
outputSuccessAsJson({
|
|
186
|
+
sessionId: session.sessionId,
|
|
187
|
+
ticketId: session.ticketId,
|
|
188
|
+
agentName: session.agentName,
|
|
189
|
+
environment: session.environment,
|
|
190
|
+
containerId: session.containerId,
|
|
191
|
+
lines,
|
|
192
|
+
content,
|
|
193
|
+
}, createMetadata('session peek', flags));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Raw text output — pipeable and scriptable
|
|
197
|
+
process.stdout.write(content + '\n');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get verified sessions from DB that have actual tmux processes.
|
|
202
|
+
* Same discovery pattern as attach.ts and list.ts.
|
|
203
|
+
*/
|
|
204
|
+
getVerifiedSessions() {
|
|
205
|
+
const sessions = [];
|
|
206
|
+
let executionStorage = null;
|
|
207
|
+
let db = null;
|
|
208
|
+
try {
|
|
209
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
210
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
211
|
+
db = new Database(dbPath);
|
|
212
|
+
executionStorage = new ExecutionStorage(db);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Not in workspace — can still discover tmux sessions
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const hostTmuxSessions = getHostTmuxSessionNames();
|
|
219
|
+
const containerTmuxSessions = getContainerTmuxSessionMap();
|
|
220
|
+
const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
|
|
221
|
+
const matchedHostSessions = new Set();
|
|
222
|
+
const matchedContainerSessions = new Set();
|
|
223
|
+
// Get active executions from DB
|
|
224
|
+
const activeExecutions = executionStorage ? [
|
|
225
|
+
...(executionStorage.listExecutions({ status: 'running' }) || []),
|
|
226
|
+
...(executionStorage.listExecutions({ status: 'starting' }) || []),
|
|
227
|
+
] : [];
|
|
228
|
+
for (const exec of activeExecutions) {
|
|
229
|
+
const isContainer = exec.environment === 'devcontainer';
|
|
230
|
+
let exists = false;
|
|
231
|
+
let containerId;
|
|
232
|
+
let actualSessionId = exec.sessionId;
|
|
233
|
+
if (!exec.sessionId) {
|
|
234
|
+
if (isContainer && exec.containerId) {
|
|
235
|
+
const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
|
|
236
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
|
|
237
|
+
if (match) {
|
|
238
|
+
actualSessionId = match;
|
|
239
|
+
exists = true;
|
|
240
|
+
containerId = exec.containerId;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
|
|
245
|
+
if (match) {
|
|
246
|
+
actualSessionId = match;
|
|
247
|
+
exists = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!actualSessionId)
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (isContainer && exec.containerId) {
|
|
255
|
+
const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
|
|
256
|
+
exists = containerSessions.includes(exec.sessionId);
|
|
257
|
+
containerId = exec.containerId;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
exists = hostTmuxSessions.includes(exec.sessionId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (exists && actualSessionId) {
|
|
264
|
+
if (isContainer && containerId) {
|
|
265
|
+
matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
matchedHostSessions.add(actualSessionId);
|
|
269
|
+
}
|
|
270
|
+
sessions.push({
|
|
271
|
+
sessionId: actualSessionId,
|
|
272
|
+
ticketId: exec.ticketId,
|
|
273
|
+
agentName: exec.agentName,
|
|
274
|
+
environment: isContainer ? 'container' : 'host',
|
|
275
|
+
containerId,
|
|
276
|
+
source: 'db',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Discover orphan sessions matching prlt pattern
|
|
281
|
+
for (const sessionName of hostTmuxSessions) {
|
|
282
|
+
if (matchedHostSessions.has(sessionName))
|
|
283
|
+
continue;
|
|
284
|
+
const parsed = parseSessionName(sessionName);
|
|
285
|
+
if (parsed) {
|
|
286
|
+
sessions.push({
|
|
287
|
+
sessionId: sessionName,
|
|
288
|
+
ticketId: parsed.ticketId,
|
|
289
|
+
agentName: parsed.agentName,
|
|
290
|
+
environment: 'host',
|
|
291
|
+
source: 'discovered',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const { sessionName, containerId } of allContainerSessions) {
|
|
296
|
+
if (matchedContainerSessions.has(`${containerId}:${sessionName}`))
|
|
297
|
+
continue;
|
|
298
|
+
const parsed = parseSessionName(sessionName);
|
|
299
|
+
if (parsed) {
|
|
300
|
+
sessions.push({
|
|
301
|
+
sessionId: sessionName,
|
|
302
|
+
ticketId: parsed.ticketId,
|
|
303
|
+
agentName: parsed.agentName,
|
|
304
|
+
environment: 'container',
|
|
305
|
+
containerId,
|
|
306
|
+
source: 'discovered',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
db?.close();
|
|
313
|
+
}
|
|
314
|
+
return sessions;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class SessionPoke extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
agent: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
message: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
protected getPMOOptions(): {
|
|
15
|
+
promptIfMultiple: boolean;
|
|
16
|
+
};
|
|
17
|
+
execute(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve an agent identifier (name or ticket ID) to a running session.
|
|
20
|
+
* Looks up active executions and matches tmux sessions.
|
|
21
|
+
*/
|
|
22
|
+
private resolveAgentSession;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the tmux session for a specific execution record.
|
|
25
|
+
*/
|
|
26
|
+
private resolveSessionForExecution;
|
|
27
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { styles } from '../../lib/styles.js';
|
|
6
|
+
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
7
|
+
import { ExecutionStorage } from '../../lib/execution/index.js';
|
|
8
|
+
import { getHostTmuxSessionNames, getContainerTmuxSessionMap, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
|
|
9
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
10
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// tmux Helpers
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Send a text message to a tmux session via send-keys.
|
|
16
|
+
* Sends the text first, then Enter separately with a small delay
|
|
17
|
+
* to avoid race conditions where Enter arrives before text is rendered.
|
|
18
|
+
*/
|
|
19
|
+
function sendMessage(sessionId, message, containerId) {
|
|
20
|
+
// Escape single quotes in the message for shell safety
|
|
21
|
+
const escapedMessage = message.replace(/'/g, "'\\''");
|
|
22
|
+
// Send the text first (without Enter), using -l (literal) flag so tmux
|
|
23
|
+
// does not interpret special characters - message is delivered verbatim
|
|
24
|
+
const sendTextCmd = `tmux send-keys -l -t "${sessionId}" '${escapedMessage}'`;
|
|
25
|
+
// Then send Enter separately (Enter is a tmux key name, not literal text)
|
|
26
|
+
const sendEnterCmd = `tmux send-keys -t "${sessionId}" Enter`;
|
|
27
|
+
if (containerId) {
|
|
28
|
+
execSync(`docker exec ${containerId} bash -c '${sendTextCmd}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 });
|
|
29
|
+
// Small delay before sending Enter to avoid race conditions
|
|
30
|
+
execSync('sleep 0.1', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
31
|
+
execSync(`docker exec ${containerId} bash -c '${sendEnterCmd}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
execSync(sendTextCmd, {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
timeout: 5000,
|
|
38
|
+
});
|
|
39
|
+
// Small delay before sending Enter
|
|
40
|
+
execSync('sleep 0.1', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
41
|
+
execSync(sendEnterCmd, {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
timeout: 5000,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Command
|
|
50
|
+
// =============================================================================
|
|
51
|
+
export default class SessionPoke extends PMOCommand {
|
|
52
|
+
static description = 'Send a message to a running agent\'s Claude Code session';
|
|
53
|
+
static examples = [
|
|
54
|
+
'<%= config.bin %> session poke altman "Please focus on the tests first"',
|
|
55
|
+
'<%= config.bin %> session poke TKT-123 "Add error handling for edge cases"',
|
|
56
|
+
];
|
|
57
|
+
static args = {
|
|
58
|
+
agent: Args.string({
|
|
59
|
+
description: 'Agent name or ticket ID of the running agent',
|
|
60
|
+
required: true,
|
|
61
|
+
}),
|
|
62
|
+
message: Args.string({
|
|
63
|
+
description: 'Message to send to the agent session',
|
|
64
|
+
required: true,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
static flags = {
|
|
68
|
+
...pmoBaseFlags,
|
|
69
|
+
};
|
|
70
|
+
getPMOOptions() {
|
|
71
|
+
return { promptIfMultiple: false };
|
|
72
|
+
}
|
|
73
|
+
async execute() {
|
|
74
|
+
const { args, flags } = await this.parse(SessionPoke);
|
|
75
|
+
const jsonMode = shouldOutputJson(flags);
|
|
76
|
+
const { agent, message } = args;
|
|
77
|
+
// Resolve the agent's active session
|
|
78
|
+
const resolved = this.resolveAgentSession(agent, jsonMode, flags);
|
|
79
|
+
if (!resolved)
|
|
80
|
+
return;
|
|
81
|
+
// Send the message
|
|
82
|
+
try {
|
|
83
|
+
sendMessage(resolved.sessionId, message, resolved.containerId);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
87
|
+
// Check for specific container/session errors
|
|
88
|
+
if (errMsg.includes('no server running') || errMsg.includes('session not found') || errMsg.includes("can't find session")) {
|
|
89
|
+
if (jsonMode) {
|
|
90
|
+
outputErrorAsJson('SESSION_NOT_FOUND', `tmux session "${resolved.sessionId}" does not exist. The agent may have exited.`, createMetadata('session poke', flags));
|
|
91
|
+
}
|
|
92
|
+
this.log('');
|
|
93
|
+
this.log(styles.error(`tmux session "${resolved.sessionId}" does not exist. The agent may have exited.`));
|
|
94
|
+
this.log(styles.muted('Use `prlt session list` to see running sessions.'));
|
|
95
|
+
this.log('');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (errMsg.includes('No such container') || errMsg.includes('is not running')) {
|
|
99
|
+
if (jsonMode) {
|
|
100
|
+
outputErrorAsJson('CONTAINER_NOT_RUNNING', `Docker container for agent "${resolved.agentName}" is not running.`, createMetadata('session poke', flags));
|
|
101
|
+
}
|
|
102
|
+
this.log('');
|
|
103
|
+
this.log(styles.error(`Docker container for agent "${resolved.agentName}" is not running.`));
|
|
104
|
+
this.log(styles.muted('The container may have stopped. Check with `docker ps`.'));
|
|
105
|
+
this.log('');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Generic send failure
|
|
109
|
+
if (jsonMode) {
|
|
110
|
+
outputErrorAsJson('SEND_FAILED', `Failed to send message to agent "${resolved.agentName}": ${errMsg}`, createMetadata('session poke', flags));
|
|
111
|
+
}
|
|
112
|
+
this.log('');
|
|
113
|
+
this.log(styles.error(`Failed to send message: ${errMsg}`));
|
|
114
|
+
this.log('');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Output result
|
|
118
|
+
if (jsonMode) {
|
|
119
|
+
outputSuccessAsJson({
|
|
120
|
+
success: true,
|
|
121
|
+
agent: resolved.agentName,
|
|
122
|
+
session: resolved.sessionId,
|
|
123
|
+
message,
|
|
124
|
+
}, createMetadata('session poke', flags));
|
|
125
|
+
}
|
|
126
|
+
this.log('');
|
|
127
|
+
this.log(styles.success(`Message sent to ${resolved.agentName} (${resolved.ticketId})`));
|
|
128
|
+
this.log('');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve an agent identifier (name or ticket ID) to a running session.
|
|
132
|
+
* Looks up active executions and matches tmux sessions.
|
|
133
|
+
*/
|
|
134
|
+
resolveAgentSession(identifier, jsonMode, flags) {
|
|
135
|
+
let executionStorage = null;
|
|
136
|
+
let db = null;
|
|
137
|
+
try {
|
|
138
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
139
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
140
|
+
db = new Database(dbPath);
|
|
141
|
+
executionStorage = new ExecutionStorage(db);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
if (jsonMode) {
|
|
145
|
+
outputErrorAsJson('NOT_IN_WORKSPACE', 'Not in a workspace. Run from a proletariat HQ directory.', createMetadata('session poke', flags));
|
|
146
|
+
}
|
|
147
|
+
this.log('');
|
|
148
|
+
this.log(styles.error('Not in a workspace. Run from a proletariat HQ directory.'));
|
|
149
|
+
this.log('');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
// Get active executions
|
|
154
|
+
const runningExecutions = executionStorage.listExecutions({ status: 'running' });
|
|
155
|
+
const startingExecutions = executionStorage.listExecutions({ status: 'starting' });
|
|
156
|
+
const activeExecutions = [...runningExecutions, ...startingExecutions];
|
|
157
|
+
// Find matching execution by exact agent name or exact ticket ID
|
|
158
|
+
const match = activeExecutions.find(exec => exec.agentName === identifier || exec.ticketId === identifier);
|
|
159
|
+
if (!match) {
|
|
160
|
+
if (jsonMode) {
|
|
161
|
+
outputErrorAsJson('NO_ACTIVE_EXECUTION', `Agent "${identifier}" has no active session. Use \`prlt session list\` to see running sessions.`, createMetadata('session poke', flags));
|
|
162
|
+
}
|
|
163
|
+
this.log('');
|
|
164
|
+
this.log(styles.error(`Agent "${identifier}" has no active session.`));
|
|
165
|
+
this.log(styles.muted('Use `prlt session list` to see running sessions.'));
|
|
166
|
+
this.log('');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return this.resolveSessionForExecution(match, jsonMode, flags);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
db?.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolve the tmux session for a specific execution record.
|
|
177
|
+
*/
|
|
178
|
+
resolveSessionForExecution(exec, jsonMode, flags) {
|
|
179
|
+
const isContainer = exec.environment === 'devcontainer';
|
|
180
|
+
let actualSessionId = exec.sessionId;
|
|
181
|
+
let containerId = isContainer ? exec.containerId : undefined;
|
|
182
|
+
// If sessionId is NULL, try to discover it from tmux
|
|
183
|
+
if (!exec.sessionId) {
|
|
184
|
+
if (isContainer && exec.containerId) {
|
|
185
|
+
const containerTmuxSessions = getContainerTmuxSessionMap();
|
|
186
|
+
const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
|
|
187
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
|
|
188
|
+
if (match) {
|
|
189
|
+
actualSessionId = match;
|
|
190
|
+
containerId = exec.containerId;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const hostTmuxSessions = getHostTmuxSessionNames();
|
|
195
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
|
|
196
|
+
if (match) {
|
|
197
|
+
actualSessionId = match;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!actualSessionId) {
|
|
202
|
+
if (jsonMode) {
|
|
203
|
+
outputErrorAsJson('SESSION_NOT_FOUND', `Could not find tmux session for agent "${exec.agentName}" (${exec.ticketId}). The session may not have started yet.`, createMetadata('session poke', flags));
|
|
204
|
+
}
|
|
205
|
+
this.log('');
|
|
206
|
+
this.log(styles.error(`Could not find tmux session for agent "${exec.agentName}" (${exec.ticketId}).`));
|
|
207
|
+
this.log(styles.muted('The session may not have started yet.'));
|
|
208
|
+
this.log('');
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
sessionId: actualSessionId,
|
|
213
|
+
ticketId: exec.ticketId,
|
|
214
|
+
agentName: exec.agentName,
|
|
215
|
+
environment: isContainer ? 'container' : 'host',
|
|
216
|
+
containerId,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -83,6 +83,35 @@ export default class SpecView extends PMOCommand {
|
|
|
83
83
|
// Get dependencies
|
|
84
84
|
const dependencies = await this.storage.getSpecDependencies(spec.id);
|
|
85
85
|
const dependents = await this.storage.getSpecDependents(spec.id);
|
|
86
|
+
// JSON output mode
|
|
87
|
+
if (jsonMode) {
|
|
88
|
+
this.log(JSON.stringify({
|
|
89
|
+
success: true,
|
|
90
|
+
spec: {
|
|
91
|
+
id: spec.id,
|
|
92
|
+
title: spec.title,
|
|
93
|
+
status: spec.status,
|
|
94
|
+
type: spec.type,
|
|
95
|
+
problem: spec.problem,
|
|
96
|
+
solution: spec.solution,
|
|
97
|
+
decisions: spec.decisions,
|
|
98
|
+
notNow: spec.notNow,
|
|
99
|
+
uiUx: spec.uiUx,
|
|
100
|
+
acceptanceCriteria: spec.acceptanceCriteria,
|
|
101
|
+
openQuestions: spec.openQuestions,
|
|
102
|
+
requirementsFunctional: spec.requirementsFunctional,
|
|
103
|
+
requirementsTechnical: spec.requirementsTechnical,
|
|
104
|
+
context: spec.context,
|
|
105
|
+
tags: spec.tags,
|
|
106
|
+
createdAt: spec.createdAt.toISOString(),
|
|
107
|
+
updatedAt: spec.updatedAt.toISOString(),
|
|
108
|
+
dependencies: dependencies.map(d => ({ id: d.id, title: d.title, status: d.status })),
|
|
109
|
+
dependents: dependents.map(d => ({ id: d.id, title: d.title, status: d.status })),
|
|
110
|
+
tickets: tickets.map(t => ({ id: t.id, title: t.title, status: t.status })),
|
|
111
|
+
},
|
|
112
|
+
}, null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
86
115
|
// Display spec info
|
|
87
116
|
this.log(styles.title(`\n📄 ${spec.title}`));
|
|
88
117
|
this.log(styles.muted('═'.repeat(60)));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
|
+
import { shouldOutputJson } from '../../lib/prompt-json.js';
|
|
3
4
|
import { styles } from '../../lib/styles.js';
|
|
4
5
|
export default class TemplateList extends PMOCommand {
|
|
5
6
|
static description = 'List all templates (ticket and phase)';
|
|
@@ -44,7 +45,7 @@ export default class TemplateList extends PMOCommand {
|
|
|
44
45
|
showTicket ? this.storage.listTicketTemplates(builtinFilter) : Promise.resolve([]),
|
|
45
46
|
showPhase ? this.storage.listPhaseTemplates(builtinFilter) : Promise.resolve([]),
|
|
46
47
|
]);
|
|
47
|
-
if (flags
|
|
48
|
+
if (shouldOutputJson(flags)) {
|
|
48
49
|
const result = {};
|
|
49
50
|
if (showTicket)
|
|
50
51
|
result.ticket = ticketTemplates;
|
|
@@ -2,6 +2,10 @@ import { Command } from '@oclif/core';
|
|
|
2
2
|
export default class ThemeAddNames extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
5
9
|
static args: {
|
|
6
10
|
theme: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
11
|
names: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
@@ -3,12 +3,17 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
4
4
|
import { isValidAgentName, normalizeAgentName } from '../../lib/themes.js';
|
|
5
5
|
import { getTheme, addThemeNames, getThemeNames } from '../../lib/database/index.js';
|
|
6
|
+
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
7
|
+
import { shouldOutputJson } from '../../lib/prompt-json.js';
|
|
6
8
|
export default class ThemeAddNames extends Command {
|
|
7
9
|
static description = 'Add names to a theme';
|
|
8
10
|
static examples = [
|
|
9
11
|
'<%= config.bin %> <%= command.id %> greek-gods zeus athena poseidon',
|
|
10
12
|
'<%= config.bin %> <%= command.id %> my-theme agent-a agent-b',
|
|
11
13
|
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...machineOutputFlags,
|
|
16
|
+
};
|
|
12
17
|
static args = {
|
|
13
18
|
theme: Args.string({
|
|
14
19
|
description: 'Theme ID',
|
|
@@ -21,7 +26,8 @@ export default class ThemeAddNames extends Command {
|
|
|
21
26
|
};
|
|
22
27
|
static strict = false; // Allow multiple name arguments
|
|
23
28
|
async run() {
|
|
24
|
-
const { args, argv } = await this.parse(ThemeAddNames);
|
|
29
|
+
const { args, argv, flags } = await this.parse(ThemeAddNames);
|
|
30
|
+
const jsonMode = shouldOutputJson(flags);
|
|
25
31
|
try {
|
|
26
32
|
const workspaceInfo = getWorkspaceInfo();
|
|
27
33
|
// Validate theme exists
|
|
@@ -60,6 +66,10 @@ export default class ThemeAddNames extends Command {
|
|
|
60
66
|
addThemeNames(workspaceInfo.path, theme.id, validNames);
|
|
61
67
|
// Get updated count
|
|
62
68
|
const allNames = getThemeNames(workspaceInfo.path, theme.id);
|
|
69
|
+
if (jsonMode) {
|
|
70
|
+
this.log(JSON.stringify({ type: 'success', result: { theme: args.theme, added: validNames, totalNames: allNames.length } }, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
63
73
|
this.log(chalk.green(`\n Added ${validNames.length} name(s) to ${theme.display_name}:`));
|
|
64
74
|
this.log(chalk.gray(` ${validNames.join(', ')}`));
|
|
65
75
|
this.log(chalk.gray(`\n Theme now has ${allNames.length} names total.`));
|
|
@@ -8,6 +8,8 @@ export default class ThemeCreate extends Command {
|
|
|
8
8
|
static flags: {
|
|
9
9
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
'display-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
};
|
|
12
14
|
run(): Promise<void>;
|
|
13
15
|
}
|