@pennyfarthing/cyclist 9.3.0 → 10.0.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.
- package/dist/api/hook-request.d.ts +11 -0
- package/dist/api/hook-request.d.ts.map +1 -1
- package/dist/api/hook-request.js +126 -28
- package/dist/api/hook-request.js.map +1 -1
- package/dist/api/hotspots.d.ts +3 -0
- package/dist/api/hotspots.d.ts.map +1 -0
- package/dist/api/hotspots.js +54 -0
- package/dist/api/hotspots.js.map +1 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/permissions.d.ts +16 -0
- package/dist/api/permissions.d.ts.map +1 -0
- package/dist/api/permissions.js +67 -0
- package/dist/api/permissions.js.map +1 -0
- package/dist/api/settings.d.ts +1 -1
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +44 -17
- package/dist/api/settings.js.map +1 -1
- package/dist/api/theme-agents.d.ts +4 -0
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +3 -0
- package/dist/api/theme-agents.js.map +1 -1
- package/dist/approval-gate.d.ts +3 -75
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +4 -121
- package/dist/approval-gate.js.map +1 -1
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +60 -0
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/cyclist-pretooluse-hook.js +57 -0
- package/dist/hooks/cyclist-pretooluse-hook.js.map +1 -0
- package/dist/hooks/pretooluse-hook.d.ts +89 -0
- package/dist/hooks/pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/pretooluse-hook.js +235 -0
- package/dist/hooks/pretooluse-hook.js.map +1 -0
- package/dist/main.d.ts +1 -134
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +42 -373
- package/dist/main.js.map +1 -1
- package/dist/menu-builder.d.ts +7 -1
- package/dist/menu-builder.d.ts.map +1 -1
- package/dist/menu-builder.js +36 -1
- package/dist/menu-builder.js.map +1 -1
- package/dist/otlp-receiver.d.ts.map +1 -1
- package/dist/otlp-receiver.js +6 -0
- package/dist/otlp-receiver.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +42 -42
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -3
- package/dist/server.js.map +1 -1
- package/dist/settings-store.d.ts +3 -1
- package/dist/settings-store.d.ts.map +1 -1
- package/dist/settings-store.js +18 -9
- package/dist/settings-store.js.map +1 -1
- package/dist/story-parser.d.ts +17 -0
- package/dist/story-parser.d.ts.map +1 -1
- package/dist/story-parser.js +183 -13
- package/dist/story-parser.js.map +1 -1
- package/dist/websocket.d.ts +1 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +48 -5
- package/dist/websocket.js.map +1 -1
- package/dist/workflow-presets.d.ts +72 -0
- package/dist/workflow-presets.d.ts.map +1 -0
- package/dist/workflow-presets.js +93 -0
- package/dist/workflow-presets.js.map +1 -0
- package/package.json +2 -2
- package/src/public/App.tsx +61 -1
- package/src/public/components/ApprovalModal/index.tsx +31 -1
- package/src/public/components/ControlBar.tsx +19 -20
- package/src/public/components/DockviewWorkspace.tsx +39 -5
- package/src/public/components/FontPicker/index.tsx +118 -33
- package/src/public/components/FullFileTree.tsx +223 -0
- package/src/public/components/Message.tsx +89 -11
- package/src/public/components/MessageView.tsx +206 -93
- package/src/public/components/PersonaHeader.tsx +47 -15
- package/src/public/components/SubagentSpan.tsx +15 -8
- package/src/public/components/panels/BackgroundPanel.tsx +1 -1
- package/src/public/components/panels/ChangedPanel.tsx +30 -44
- package/src/public/components/panels/HotspotsPanel.tsx +365 -0
- package/src/public/components/panels/MessagePanel.tsx +79 -5
- package/src/public/components/panels/SettingsPanel.tsx +3 -28
- package/src/public/components/panels/WorkflowPanel.tsx +108 -13
- package/src/public/components/panels/index.ts +1 -0
- package/src/public/contexts/ClaudeContext.tsx +16 -1
- package/src/public/css/theme-system.css +46 -38
- package/src/public/hooks/useColorScheme.ts +27 -0
- package/src/public/hooks/useFileBrowser.ts +71 -0
- package/src/public/hooks/useHotspots.ts +113 -0
- package/src/public/hooks/usePlanModeExit.ts +105 -0
- package/src/public/hooks/useStory.ts +12 -3
- package/src/public/images/cyclist-dark.png +0 -0
- package/src/public/images/cyclist-light.png +0 -0
- package/src/public/styles/dockview-theme.css +31 -33
- package/src/public/styles/tailwind.css +417 -58
- package/src/public/types/message.ts +6 -1
- package/src/public/utils/markdown.ts +2 -2
- package/src/public/utils/slash-commands.ts +1 -1
- package/src/public/utils/toolStackGrouper.ts +5 -6
package/dist/main.js
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
* This module exports testable functions and constants for unit testing,
|
|
9
9
|
* while the Electron-specific runtime code only executes in Electron context.
|
|
10
10
|
*/
|
|
11
|
-
import { createServer as createHttpServer } from 'http';
|
|
12
11
|
import { fileURLToPath } from 'url';
|
|
13
12
|
import { dirname, join, basename } from 'path';
|
|
14
13
|
import { getCurrentPersona, detectPennyfarthingProject, watchAgentChanges } from './pennyfarthing.js';
|
|
15
|
-
import { getStoryInfo, getAllReposGitInfoAsync, writePortFile, cleanupPortFile, writePidFile, cleanupPidFile, readPidFile, isProcessRunning, getOtelConfig
|
|
14
|
+
import { getStoryInfo, getAllReposGitInfoAsync, writePortFile, cleanupPortFile, writePidFile, cleanupPidFile, readPidFile, isProcessRunning, getOtelConfig } from './server.js';
|
|
16
15
|
import { parseToolStats, createEmptyStats } from './tool-stats.js';
|
|
17
|
-
import { getTokenStats, setTokenStatsCallback, setToolEventCallback, resetTokenStats, resetEventStore, getToolEventsFiltered, getToolTypes, exportAuditLogAsJSON, exportAuditLogAsCSV, getAuditLogStats, getUserEmail, setUserEmailCallback, setBackgroundTaskCallback, setBackgroundTaskStartCallback, trackBackgroundTask, getBackgroundTaskByToolId, getBackgroundTasks, } from './otlp-receiver.js';
|
|
16
|
+
import { getTokenStats, setTokenStatsCallback, setToolEventCallback, resetTokenStats, resetEventStore, getToolEventsFiltered, getToolTypes, exportAuditLogAsJSON, exportAuditLogAsCSV, getAuditLogStats, getUserEmail, setUserEmailCallback, setBackgroundTaskCallback, setBackgroundTaskStartCallback, trackBackgroundTask, completeBackgroundTask, getBackgroundTaskByToolId, getBackgroundTasks, } from './otlp-receiver.js';
|
|
18
17
|
import { ClaudeService } from './claude-service.js';
|
|
19
18
|
import { selectContextTier, getPrimeContextJson } from './prime.js';
|
|
20
19
|
import { isTodoWriteMessage, extractTodos } from './todos.js';
|
|
@@ -27,10 +26,8 @@ import { getVerboseMode, setVerboseMode } from './settings-store.js';
|
|
|
27
26
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
28
27
|
import { getCurrentSettings, saveUserSettings, initializeSettings, loadGrants, saveGrants, } from './settings.js';
|
|
29
28
|
import { broadcastBackgroundTaskEvent } from './api/background-tasks.js';
|
|
30
|
-
import { setStoryUpdateCallback, setGitUpdateCallback, broadcastClaudeMessage, setClaudeSendCallback, setClaudeAbortCallback, setClaudeClearCallback, setClaudeSetModeCallback, setClaudeGetModeCallback, setClaudeClearAndReloadCallback, broadcastTodosUpdate, broadcastContextUpdate } from './websocket.js';
|
|
31
|
-
import { initializeGrants, setGrantsPersistCallback } from './settings-store.js';
|
|
32
|
-
// Story 33-7: Import approval gate functions for tool execution pipeline
|
|
33
|
-
import { interceptToolUse, requestApproval, createRejectionError, } from './approval-gate.js';
|
|
29
|
+
import { setStoryUpdateCallback, setGitUpdateCallback, broadcastClaudeMessage, setClaudeSendCallback, setClaudeAbortCallback, setClaudeClearCallback, setClaudeSetModeCallback, setClaudeGetModeCallback, setClaudeClearAndReloadCallback, broadcastTodosUpdate, broadcastContextUpdate, broadcastPanelToggle } from './websocket.js';
|
|
30
|
+
import { initializeGrants, setGrantsPersistCallback, clearSessionGrants } from './settings-store.js';
|
|
34
31
|
import { openSettingsWindow, setMainWindowRef, setBrowserWindowRef } from './settings-window.js';
|
|
35
32
|
import { setBellMode } from './bell-mode.js';
|
|
36
33
|
import { IPC_DATA_CHANNELS, IPC_CLAUDE_CHANNELS, IPC_AGENT_CHANNELS, IPC_SETTINGS_CHANNELS, IPC_AUDIT_LOG_CHANNELS, IPC_FILE_BROWSER_CHANNELS, IPC_COMMAND_CHANNELS, IPC_BACKGROUND_TASK_CHANNELS, IPC_SKILL_CHANNELS, IPC_LAYOUT_CHANNELS, IPC_AVATAR_CHANNELS, } from './ipc-channels.js';
|
|
@@ -65,8 +62,9 @@ catch {
|
|
|
65
62
|
// Re-export IPC channels from dedicated module
|
|
66
63
|
export { IPC_DATA_CHANNELS, IPC_CLAUDE_CHANNELS, IPC_AGENT_CHANNELS, IPC_DIFF_CHANNELS, IPC_SETTINGS_CHANNELS, IPC_AUDIT_LOG_CHANNELS, IPC_FILE_BROWSER_CHANNELS, IPC_COMMAND_CHANNELS, IPC_BACKGROUND_TASK_CHANNELS, IPC_SKILL_CHANNELS, IPC_CONTEXT_CLEAR_CHANNELS, IPC_LAYOUT_CHANNELS, IPC_AVATAR_CHANNELS, } from './ipc-channels.js';
|
|
67
64
|
// Re-export menu builders from dedicated module
|
|
68
|
-
export { AGENT_DEFINITIONS, WORKFLOW_DEFINITIONS, buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, getMenuTemplate, } from './menu-builder.js';
|
|
69
|
-
|
|
65
|
+
export { AGENT_DEFINITIONS, WORKFLOW_DEFINITIONS, buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, getMenuTemplate, setPanelToggleBroadcast, } from './menu-builder.js';
|
|
66
|
+
// Local imports for menu building
|
|
67
|
+
import { buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, setPanelToggleBroadcast, } from './menu-builder.js';
|
|
70
68
|
/**
|
|
71
69
|
* Get list of registered data IPC channels (for testing)
|
|
72
70
|
* Returns the data channels that setupDataIPCHandlers will register
|
|
@@ -794,6 +792,31 @@ export function startProjectWatchers() {
|
|
|
794
792
|
// MSSCI-14190: Process tool_use messages for diff tracking (same as IPC handler)
|
|
795
793
|
// This was missing from the WebSocket callback path!
|
|
796
794
|
processToolUseFromMessage(message);
|
|
795
|
+
// Complete subagent tasks when tool_result arrives
|
|
796
|
+
// CLI format: discrete tool_result message
|
|
797
|
+
if (message.type === 'tool_result') {
|
|
798
|
+
const msg = message;
|
|
799
|
+
if (msg.tool_id) {
|
|
800
|
+
const task = getBackgroundTaskByToolId(msg.tool_id);
|
|
801
|
+
if (task) {
|
|
802
|
+
completeBackgroundTask(msg.tool_id, !msg.is_error, msg.is_error ? undefined : msg.output?.slice(0, 500), msg.is_error ? (msg.output?.slice(0, 500) || 'Task failed') : undefined);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// SDK format: tool_result nested in user message content
|
|
807
|
+
if (message.type === 'user') {
|
|
808
|
+
const userMsg = message;
|
|
809
|
+
if (userMsg.message?.content) {
|
|
810
|
+
for (const block of userMsg.message.content) {
|
|
811
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
812
|
+
const task = getBackgroundTaskByToolId(block.tool_use_id);
|
|
813
|
+
if (task) {
|
|
814
|
+
completeBackgroundTask(block.tool_use_id, !block.is_error, block.is_error ? undefined : (typeof block.content === 'string' ? block.content.slice(0, 500) : undefined), block.is_error ? (typeof block.content === 'string' ? block.content.slice(0, 500) : 'Task failed') : undefined);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
797
820
|
}
|
|
798
821
|
onComplete();
|
|
799
822
|
broadcastToRenderer(IPC_CLAUDE_CHANNELS.CLAUDE_COMPLETE, null);
|
|
@@ -951,16 +974,18 @@ function processToolUseFromMessage(message) {
|
|
|
951
974
|
// Store for OTEL correlation
|
|
952
975
|
if (toolId && toolInput) {
|
|
953
976
|
storePendingToolInput(toolId, toolName, toolInput);
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
977
|
+
// Track all Task tool subagents (background and foreground)
|
|
978
|
+
// All must be tracked so enrichMessageWithSubagentContext can look them up
|
|
979
|
+
if (toolName === 'Task') {
|
|
980
|
+
const description = toolInput.description || toolInput.prompt?.substring(0, 50) || 'Subagent task';
|
|
957
981
|
const subagentType = toolInput.subagent_type || 'general-purpose';
|
|
982
|
+
const isBackground = toolInput.run_in_background === true;
|
|
958
983
|
trackBackgroundTask({
|
|
959
984
|
taskId: toolId,
|
|
960
985
|
description,
|
|
961
986
|
subagentType,
|
|
962
987
|
startedAt: Date.now(),
|
|
963
|
-
isBackground
|
|
988
|
+
isBackground,
|
|
964
989
|
});
|
|
965
990
|
}
|
|
966
991
|
}
|
|
@@ -1506,363 +1531,6 @@ export function setupCommandIPCHandlers(ipcMain) {
|
|
|
1506
1531
|
registeredCommandChannels = [IPC_COMMAND_CHANNELS.EXECUTE];
|
|
1507
1532
|
console.log('Command IPC handlers registered');
|
|
1508
1533
|
}
|
|
1509
|
-
// Dependency injection for testing
|
|
1510
|
-
let ipcSender = null;
|
|
1511
|
-
let toolExecutor = null;
|
|
1512
|
-
let errorInjector = null;
|
|
1513
|
-
/**
|
|
1514
|
-
* Set the IPC sender function (for testing)
|
|
1515
|
-
*/
|
|
1516
|
-
export function setIPCSender(sender) {
|
|
1517
|
-
ipcSender = sender;
|
|
1518
|
-
}
|
|
1519
|
-
/**
|
|
1520
|
-
* Set the tool executor function (for testing)
|
|
1521
|
-
*/
|
|
1522
|
-
export function setToolExecutor(executor) {
|
|
1523
|
-
toolExecutor = executor;
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Set the error injector function (for testing)
|
|
1527
|
-
*/
|
|
1528
|
-
export function setErrorInjector(injector) {
|
|
1529
|
-
errorInjector = injector;
|
|
1530
|
-
}
|
|
1531
|
-
/**
|
|
1532
|
-
* Send an approval request to the renderer via IPC
|
|
1533
|
-
*/
|
|
1534
|
-
export function sendApprovalRequest(toolId, toolName, context) {
|
|
1535
|
-
const sender = ipcSender || broadcastToRenderer;
|
|
1536
|
-
sender('permission-request', {
|
|
1537
|
-
toolId,
|
|
1538
|
-
toolName,
|
|
1539
|
-
context,
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
/**
|
|
1543
|
-
* Handle permission response from renderer
|
|
1544
|
-
* Called by IPC handler when user responds to approval modal
|
|
1545
|
-
*/
|
|
1546
|
-
export function handlePermissionResponse(response) {
|
|
1547
|
-
// Story 33-7: First try to resolve hook approval (from PreToolUse hook)
|
|
1548
|
-
// This is the path that actually controls tool execution
|
|
1549
|
-
resolveHookApproval(response.toolId, response.approved, response.grantScope);
|
|
1550
|
-
// Also resolve approval-gate.js pending approvals for backwards compatibility
|
|
1551
|
-
// (This was the old observer-only path)
|
|
1552
|
-
import('./approval-gate.js').then(({ resolveApproval }) => {
|
|
1553
|
-
resolveApproval(response.toolId, response.approved, response.grantScope);
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Process a tool_use message with approval gate check
|
|
1558
|
-
* This is the main integration point for story 33-7
|
|
1559
|
-
*
|
|
1560
|
-
* @param message - The tool_use message to process
|
|
1561
|
-
* @returns ApprovalResult indicating whether approval is needed and outcome
|
|
1562
|
-
*/
|
|
1563
|
-
export async function processToolUseWithApproval(message) {
|
|
1564
|
-
// Check if this tool_use needs approval
|
|
1565
|
-
const interceptResult = interceptToolUse(message);
|
|
1566
|
-
// If gate is disabled or grant exists, pass through immediately
|
|
1567
|
-
if (!interceptResult.shouldApprove) {
|
|
1568
|
-
// Execute tool if executor is set
|
|
1569
|
-
if (toolExecutor) {
|
|
1570
|
-
toolExecutor(message);
|
|
1571
|
-
}
|
|
1572
|
-
return {
|
|
1573
|
-
needsApproval: false,
|
|
1574
|
-
passThrough: true,
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
// Need approval - send IPC request and wait for response
|
|
1578
|
-
sendApprovalRequest(interceptResult.toolId, interceptResult.toolName, interceptResult.context);
|
|
1579
|
-
// Get the command for Bash tools, or use context for other tools
|
|
1580
|
-
const command = interceptResult.toolName === 'Bash'
|
|
1581
|
-
? interceptResult.context.command || ''
|
|
1582
|
-
: JSON.stringify(interceptResult.context);
|
|
1583
|
-
// Wait for user response
|
|
1584
|
-
const approved = await requestApproval(command, interceptResult.toolId);
|
|
1585
|
-
if (approved) {
|
|
1586
|
-
// User approved - execute tool
|
|
1587
|
-
if (toolExecutor) {
|
|
1588
|
-
toolExecutor(message);
|
|
1589
|
-
}
|
|
1590
|
-
return {
|
|
1591
|
-
needsApproval: true,
|
|
1592
|
-
passThrough: true,
|
|
1593
|
-
approved: true,
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
else {
|
|
1597
|
-
// User rejected - create and inject error
|
|
1598
|
-
const errorMessage = createRejectionError(interceptResult.toolId);
|
|
1599
|
-
if (errorInjector) {
|
|
1600
|
-
errorInjector(errorMessage);
|
|
1601
|
-
}
|
|
1602
|
-
return {
|
|
1603
|
-
needsApproval: true,
|
|
1604
|
-
passThrough: false,
|
|
1605
|
-
approved: false,
|
|
1606
|
-
rejected: true,
|
|
1607
|
-
errorMessage,
|
|
1608
|
-
};
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Set up IPC handlers for approval gate
|
|
1613
|
-
* Story 33-7: Handles permission request/response flow
|
|
1614
|
-
*/
|
|
1615
|
-
export function setupApprovalIPCHandlers(ipcMain) {
|
|
1616
|
-
// Handle permission response from renderer
|
|
1617
|
-
if (ipcMain.on) {
|
|
1618
|
-
ipcMain.on('permission-response', (_event, response) => {
|
|
1619
|
-
handlePermissionResponse(response);
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
if (ipcMain.handle) {
|
|
1623
|
-
ipcMain.handle('permission-response', async (_event, response) => {
|
|
1624
|
-
handlePermissionResponse(response);
|
|
1625
|
-
return { success: true };
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
console.log('Approval gate IPC handlers registered');
|
|
1629
|
-
}
|
|
1630
|
-
// =============================================================================
|
|
1631
|
-
// Story 33-7: Approval Hook Server
|
|
1632
|
-
// =============================================================================
|
|
1633
|
-
// HTTP server that receives approval requests from the PreToolUse hook script.
|
|
1634
|
-
// The hook runs in Claude Code's process, sends requests here, we show modal,
|
|
1635
|
-
// user decides, we respond, hook tells Claude Code to allow/deny.
|
|
1636
|
-
//
|
|
1637
|
-
// Multi-instance support: Uses dynamic port selection with .cyclist-approval-port
|
|
1638
|
-
// discovery file to prevent cross-instance interference when multiple Cyclist
|
|
1639
|
-
// windows are open for different projects.
|
|
1640
|
-
let approvalServer = null;
|
|
1641
|
-
let approvalServerPort = null;
|
|
1642
|
-
// Pending approval requests from hooks, keyed by toolId
|
|
1643
|
-
// MSSCI-11947: Extended to support data field for interactive tools
|
|
1644
|
-
const pendingHookApprovals = new Map();
|
|
1645
|
-
/**
|
|
1646
|
-
* Handle incoming approval request from hook script
|
|
1647
|
-
*/
|
|
1648
|
-
async function handleHookApprovalRequest(toolName, toolId, input) {
|
|
1649
|
-
// Check if gate is enabled
|
|
1650
|
-
const { getBashApprovalGate, checkGrant, isAllowlisted } = await import('./settings-store.js');
|
|
1651
|
-
if (!getBashApprovalGate()) {
|
|
1652
|
-
return { decision: 'allow', reason: 'Approval gate disabled' };
|
|
1653
|
-
}
|
|
1654
|
-
// Check allowlist and grants
|
|
1655
|
-
if (toolName === 'Bash') {
|
|
1656
|
-
const command = input.command || '';
|
|
1657
|
-
if (isAllowlisted(command) || checkGrant('Bash', command)) {
|
|
1658
|
-
return { decision: 'allow', reason: 'Matched allowlist or existing grant' };
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
// Need user approval - send to renderer and wait
|
|
1662
|
-
return new Promise((resolve) => {
|
|
1663
|
-
pendingHookApprovals.set(toolId, { resolve, toolName, input });
|
|
1664
|
-
// Send approval request to renderer
|
|
1665
|
-
broadcastToRenderer('permission-request', {
|
|
1666
|
-
toolId,
|
|
1667
|
-
toolName,
|
|
1668
|
-
context: input,
|
|
1669
|
-
source: 'hook', // Indicate this came from hook, not observation
|
|
1670
|
-
});
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
/**
|
|
1674
|
-
* Resolve a pending hook approval (called when user responds to modal)
|
|
1675
|
-
*/
|
|
1676
|
-
export function resolveHookApproval(toolId, approved, grantScope) {
|
|
1677
|
-
const pending = pendingHookApprovals.get(toolId);
|
|
1678
|
-
if (pending) {
|
|
1679
|
-
// Add grant if approved with scope
|
|
1680
|
-
if (approved && grantScope && pending.toolName === 'Bash') {
|
|
1681
|
-
import('./settings-store.js').then(({ addGrant, extractPattern }) => {
|
|
1682
|
-
const command = pending.input.command || '';
|
|
1683
|
-
const pattern = extractPattern(command);
|
|
1684
|
-
addGrant({
|
|
1685
|
-
tool: 'Bash',
|
|
1686
|
-
scope: pattern,
|
|
1687
|
-
grant_type: grantScope,
|
|
1688
|
-
granted_at: new Date().toISOString(),
|
|
1689
|
-
});
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
pending.resolve({
|
|
1693
|
-
decision: approved ? 'allow' : 'deny',
|
|
1694
|
-
reason: approved ? `Approved by user (${grantScope || 'once'})` : 'Rejected by user',
|
|
1695
|
-
});
|
|
1696
|
-
pendingHookApprovals.delete(toolId);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
// =============================================================================
|
|
1700
|
-
// MSSCI-11947: Hook Response Data Channel
|
|
1701
|
-
// =============================================================================
|
|
1702
|
-
// Functions for handling interactive tools (AskUserQuestion, ExitPlanMode)
|
|
1703
|
-
// that need to return structured data back to Claude, not just allow/deny.
|
|
1704
|
-
/**
|
|
1705
|
-
* Check if a tool_use is an interactive tool that needs data return
|
|
1706
|
-
* MSSCI-11947: AC1 - Detect AskUserQuestion and ExitPlanMode
|
|
1707
|
-
*/
|
|
1708
|
-
export function isInteractiveToolUse(toolUse) {
|
|
1709
|
-
const interactiveTools = ['AskUserQuestion', 'ExitPlanMode'];
|
|
1710
|
-
return toolUse.type === 'tool_use' && interactiveTools.includes(toolUse.tool_name || '');
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Process an interactive tool_use and wait for user response with data
|
|
1714
|
-
* MSSCI-11947: AC1 - Handle interactive tool approval flow
|
|
1715
|
-
*/
|
|
1716
|
-
export function processInteractiveToolUse(toolUse) {
|
|
1717
|
-
const toolId = toolUse.tool_id || `interactive-${Date.now()}`;
|
|
1718
|
-
const toolName = toolUse.tool_name || 'Unknown';
|
|
1719
|
-
const input = toolUse.input || {};
|
|
1720
|
-
return new Promise((resolve) => {
|
|
1721
|
-
pendingHookApprovals.set(toolId, { resolve, toolName, input });
|
|
1722
|
-
// Send approval request to renderer with full tool input
|
|
1723
|
-
broadcastToRenderer('permission-request', {
|
|
1724
|
-
toolId,
|
|
1725
|
-
toolName,
|
|
1726
|
-
context: input,
|
|
1727
|
-
source: 'hook',
|
|
1728
|
-
});
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
/**
|
|
1732
|
-
* Resolve a pending hook approval with data (for interactive tools)
|
|
1733
|
-
* MSSCI-11947: AC1 - Extended resolve that includes data field
|
|
1734
|
-
*/
|
|
1735
|
-
export function resolveHookApprovalWithData(toolId, approved, grantScope, data) {
|
|
1736
|
-
const pending = pendingHookApprovals.get(toolId);
|
|
1737
|
-
if (pending) {
|
|
1738
|
-
// For interactive tools, we don't create grants (they're one-time responses)
|
|
1739
|
-
pending.resolve({
|
|
1740
|
-
decision: approved ? 'allow' : 'deny',
|
|
1741
|
-
reason: approved ? `Approved by user (${grantScope || 'once'})` : 'Rejected by user',
|
|
1742
|
-
data: data || {},
|
|
1743
|
-
});
|
|
1744
|
-
pendingHookApprovals.delete(toolId);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
/**
|
|
1748
|
-
* Format hook response with optional data field
|
|
1749
|
-
* MSSCI-11947: AC4 - Format response for hook output
|
|
1750
|
-
*/
|
|
1751
|
-
export function formatHookResponseWithData(decision, reason, data) {
|
|
1752
|
-
const response = {
|
|
1753
|
-
decision,
|
|
1754
|
-
reason,
|
|
1755
|
-
};
|
|
1756
|
-
if (data !== undefined) {
|
|
1757
|
-
response.data = data;
|
|
1758
|
-
}
|
|
1759
|
-
return response;
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* Format answers as updatedInput for AskUserQuestion
|
|
1763
|
-
* MSSCI-11947: AC4 - Format for hook updatedInput
|
|
1764
|
-
*/
|
|
1765
|
-
export function formatUpdatedInputForAskUserQuestion(answers) {
|
|
1766
|
-
return { answers };
|
|
1767
|
-
}
|
|
1768
|
-
/**
|
|
1769
|
-
* Format plan response as updatedInput for ExitPlanMode
|
|
1770
|
-
* MSSCI-11947: AC4 - Format for hook updatedInput
|
|
1771
|
-
*/
|
|
1772
|
-
export function formatUpdatedInputForExitPlanMode(response) {
|
|
1773
|
-
return response;
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* Serialize approval data for transmission
|
|
1777
|
-
* MSSCI-11947: AC1 - JSON serialization helper
|
|
1778
|
-
*/
|
|
1779
|
-
export function serializeApprovalData(data) {
|
|
1780
|
-
return JSON.stringify(data);
|
|
1781
|
-
}
|
|
1782
|
-
/**
|
|
1783
|
-
* Deserialize approval data from transmission
|
|
1784
|
-
* MSSCI-11947: AC1 - JSON deserialization helper
|
|
1785
|
-
*/
|
|
1786
|
-
export function deserializeApprovalData(data) {
|
|
1787
|
-
return JSON.parse(data);
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Start the approval hook server with dynamic port selection
|
|
1791
|
-
* Uses port 0 to let OS assign an available port (avoids race conditions)
|
|
1792
|
-
* Writes port to .cyclist-approval-port for hook discovery
|
|
1793
|
-
*/
|
|
1794
|
-
export async function startApprovalServer() {
|
|
1795
|
-
if (approvalServer) {
|
|
1796
|
-
console.log('Approval server already running');
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
const projectDir = getProjectDirectory();
|
|
1800
|
-
if (!projectDir) {
|
|
1801
|
-
console.warn('No project directory set, cannot start approval server');
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
approvalServer = createHttpServer(async (req, res) => {
|
|
1805
|
-
if (req.method === 'POST' && req.url === '/approval-request') {
|
|
1806
|
-
let body = '';
|
|
1807
|
-
req.on('data', (chunk) => { body += chunk; });
|
|
1808
|
-
req.on('end', async () => {
|
|
1809
|
-
try {
|
|
1810
|
-
const data = JSON.parse(body);
|
|
1811
|
-
const { toolName, toolId, input } = data;
|
|
1812
|
-
const response = await handleHookApprovalRequest(toolName, toolId, input);
|
|
1813
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1814
|
-
res.end(JSON.stringify(response));
|
|
1815
|
-
}
|
|
1816
|
-
catch {
|
|
1817
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1818
|
-
res.end(JSON.stringify({ error: 'Invalid request' }));
|
|
1819
|
-
}
|
|
1820
|
-
});
|
|
1821
|
-
}
|
|
1822
|
-
else {
|
|
1823
|
-
res.writeHead(404);
|
|
1824
|
-
res.end('Not found');
|
|
1825
|
-
}
|
|
1826
|
-
});
|
|
1827
|
-
// Use port 0 to let OS assign an available port (avoids race conditions)
|
|
1828
|
-
approvalServer.listen(0, '127.0.0.1', () => {
|
|
1829
|
-
const addr = approvalServer.address();
|
|
1830
|
-
approvalServerPort = typeof addr === 'object' && addr ? addr.port : null;
|
|
1831
|
-
if (approvalServerPort) {
|
|
1832
|
-
console.log(`Approval hook server running on http://127.0.0.1:${approvalServerPort}`);
|
|
1833
|
-
// Write port file for hook discovery
|
|
1834
|
-
writeApprovalPortFile(projectDir, approvalServerPort);
|
|
1835
|
-
console.log(`[33-7] Wrote .cyclist-approval-port file to ${projectDir}`);
|
|
1836
|
-
}
|
|
1837
|
-
});
|
|
1838
|
-
approvalServer.on('error', (err) => {
|
|
1839
|
-
console.error('Approval server error:', err);
|
|
1840
|
-
approvalServerPort = null;
|
|
1841
|
-
});
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Stop the approval hook server and clean up port file
|
|
1845
|
-
*/
|
|
1846
|
-
export function stopApprovalServer() {
|
|
1847
|
-
if (approvalServer) {
|
|
1848
|
-
approvalServer.close();
|
|
1849
|
-
approvalServer = null;
|
|
1850
|
-
approvalServerPort = null;
|
|
1851
|
-
// Clean up port file
|
|
1852
|
-
const projectDir = getProjectDirectory();
|
|
1853
|
-
if (projectDir) {
|
|
1854
|
-
cleanupApprovalPortFile(projectDir);
|
|
1855
|
-
console.log('[33-7] Cleaned up .cyclist-approval-port file');
|
|
1856
|
-
}
|
|
1857
|
-
console.log('Approval hook server stopped');
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
/**
|
|
1861
|
-
* Get the current approval server port (for testing)
|
|
1862
|
-
*/
|
|
1863
|
-
export function getApprovalServerPort() {
|
|
1864
|
-
return approvalServerPort;
|
|
1865
|
-
}
|
|
1866
1534
|
// =============================================================================
|
|
1867
1535
|
// Session Persistence (E7-3: AC4)
|
|
1868
1536
|
// =============================================================================
|
|
@@ -2074,8 +1742,6 @@ if (isElectron) {
|
|
|
2074
1742
|
setupAuditLogIPCHandlers(ipcMain);
|
|
2075
1743
|
setupCommandIPCHandlers(ipcMain); // 23-3: Command execution
|
|
2076
1744
|
setupSkillIPCHandlers(ipcMain); // 35-12: Skill invocation tracking
|
|
2077
|
-
setupApprovalIPCHandlers(ipcMain); // 33-7: Approval gate wiring
|
|
2078
|
-
// NOTE: startApprovalServer() moved to app.whenReady() - needs project directory
|
|
2079
1745
|
/**
|
|
2080
1746
|
* Kill orphaned Claude CLI process from previous Cyclist session in THIS project.
|
|
2081
1747
|
* B-24 fix: Only kills the specific PID from .cyclist-pid, not all Claude processes.
|
|
@@ -2225,12 +1891,13 @@ if (isElectron) {
|
|
|
2225
1891
|
// In Electron mode, Claude messages are broadcast from main.ts, not per-connection ClaudeService
|
|
2226
1892
|
process.env.CYCLIST_ELECTRON_MODE = '1';
|
|
2227
1893
|
await startServer();
|
|
2228
|
-
await startApprovalServer(); // 33-7: Start after project dir set
|
|
2229
1894
|
createWindow();
|
|
2230
1895
|
if (mainWindow && projectDir) {
|
|
2231
1896
|
mainWindow.setTitle(`Cyclist - ${basename(projectDir)}`);
|
|
2232
1897
|
}
|
|
2233
1898
|
// B-23: Wire agent and workflow menus to Electron menu bar
|
|
1899
|
+
// Wire panel toggle to WebSocket broadcast
|
|
1900
|
+
setPanelToggleBroadcast(broadcastPanelToggle);
|
|
2234
1901
|
// Use standard macOS menu roles instead of reconstructing existing menu
|
|
2235
1902
|
// (reconstructing fails on nested submenus like Window)
|
|
2236
1903
|
// 22-5: Custom View menu with Verbose Mode toggle
|
|
@@ -2298,6 +1965,8 @@ if (isElectron) {
|
|
|
2298
1965
|
if (claudeServiceInstance) {
|
|
2299
1966
|
claudeServiceInstance.abort();
|
|
2300
1967
|
}
|
|
1968
|
+
// MSSCI-14324: Clear session/once grants on shutdown
|
|
1969
|
+
clearSessionGrants();
|
|
2301
1970
|
// B-24 fix: Clean up PID file on graceful shutdown
|
|
2302
1971
|
const projectDir = getProjectDirectory();
|
|
2303
1972
|
if (projectDir) {
|