@xelauvas/xela-cli 0.3.1 → 0.4.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/bin/xela.js +0 -12
- package/local/README.md +112 -0
- package/local/start-qwen.sh +47 -0
- package/local/start-tunnel.sh +36 -0
- package/local/xela-local.sh +139 -0
- package/package.json +9 -5
- package/run.sh +33 -0
- package/src/Tool.ts +1 -1
- package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
- package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
- package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
- package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
- package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
- package/src/_shims/bun_bundle.js +79 -4
- package/src/_shims/cjs_stub.cjs +32 -10
- package/src/_shims/loader.js +104 -5
- package/src/_shims/openai_adapter.ts +446 -230
- package/src/_shims/register.js +16 -3
- package/src/assistant/gate.ts +15 -0
- package/src/assistant/index.ts +89 -0
- package/src/bootstrap/state.ts +6 -1
- package/src/bridge/peerSessions.ts +18 -0
- package/src/cli/print.ts +5 -0
- package/src/commands/assistant/assistant.ts +17 -0
- package/src/commands/assistant/index.ts +11 -0
- package/src/commands/brief.ts +3 -1
- package/src/commands/buddy/buddy.ts +88 -0
- package/src/commands/buddy/index.ts +12 -0
- package/src/commands/model/model.tsx +8 -23
- package/src/commands/peers/index.ts +11 -0
- package/src/commands/peers/peers.ts +24 -0
- package/src/commands/proactive/proactive.ts +64 -0
- package/src/commands/proactive.ts +12 -0
- package/src/commands/workflows/index.ts +12 -0
- package/src/commands/workflows/workflows.ts +36 -0
- package/src/commands.ts +4 -8
- package/src/components/Messages.tsx +1 -1
- package/src/components/ModelPicker.tsx +4 -34
- package/src/components/Spinner.tsx +54 -1
- package/src/components/StructuredDiff/colorDiff.ts +3 -1
- package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
- package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
- package/src/components/permissions/PermissionRequest.tsx +4 -2
- package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
- package/src/constants/prompts.ts +26 -0
- package/src/ink/terminal.ts +9 -1
- package/src/jobs/classifier.ts +103 -0
- package/src/memdir/paths.ts +13 -1
- package/src/proactive/index.ts +158 -0
- package/src/proactive/useProactive.ts +78 -0
- package/src/screens/REPL.tsx +28 -8
- package/src/services/api/claude.ts +6 -1
- package/src/services/compact/autoCompact.ts +18 -4
- package/src/services/compact/cachedMCConfig.ts +1 -0
- package/src/services/compact/cachedMicrocompact.ts +113 -0
- package/src/services/compact/compact.ts +22 -2
- package/src/services/compact/reactiveCompact.ts +251 -0
- package/src/services/compact/snipCompact.ts +282 -0
- package/src/services/compact/snipProjection.ts +50 -0
- package/src/services/contextCollapse/index.ts +322 -0
- package/src/services/contextCollapse/operations.ts +187 -0
- package/src/services/contextCollapse/persist.ts +40 -0
- package/src/services/extractMemories/extractMemories.ts +11 -2
- package/src/services/skillSearch/featureCheck.ts +12 -0
- package/src/services/skillSearch/localSearch.ts +98 -0
- package/src/services/skillSearch/prefetch.ts +125 -0
- package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
- package/src/services/skillSearch/remoteSkillState.ts +21 -0
- package/src/services/skillSearch/signals.ts +7 -0
- package/src/services/skillSearch/telemetry.ts +9 -0
- package/src/setup.ts +10 -5
- package/src/skills/bundled/dream.ts +31 -0
- package/src/skills/bundled/hunter.ts +45 -0
- package/src/skills/bundled/index.ts +10 -23
- package/src/skills/bundled/runSkillGenerator.ts +61 -0
- package/src/skills/mcpSkills.ts +101 -0
- package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
- package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
- package/src/tasks.ts +22 -8
- package/src/tools/AgentTool/runAgent.ts +11 -0
- package/src/tools/BriefTool/BriefTool.ts +2 -0
- package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
- package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
- package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
- package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
- package/src/tools/MCPTool/MCPTool.ts +6 -0
- package/src/tools/MonitorTool/MonitorTool.ts +273 -0
- package/src/tools/MonitorTool/UI.tsx +25 -0
- package/src/tools/MonitorTool/events.ts +84 -0
- package/src/tools/MonitorTool/prompt.ts +45 -0
- package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
- package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
- package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
- package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
- package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
- package/src/tools/SendUserFileTool/prompt.ts +7 -0
- package/src/tools/SleepTool/SleepTool.ts +126 -0
- package/src/tools/SnipTool/SnipTool.ts +90 -0
- package/src/tools/SnipTool/prompt.ts +5 -0
- package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
- package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
- package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
- package/src/tools/ToolSearchTool/prompt.ts +1 -1
- package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
- package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
- package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
- package/src/tools/WebBrowserTool/prompt.ts +16 -0
- package/src/tools/WorkflowTool/UI.tsx +27 -0
- package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
- package/src/tools/WorkflowTool/bundled/index.ts +88 -0
- package/src/tools/WorkflowTool/constants.ts +7 -0
- package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
- package/src/tools/WorkflowTool/prompt.ts +38 -0
- package/src/tools/WorkflowTool/runtime.ts +331 -0
- package/src/tools.ts +36 -31
- package/src/utils/betas.ts +5 -0
- package/src/utils/context.ts +9 -0
- package/src/utils/conversationRecovery.ts +1 -1
- package/src/utils/forkedAgent.ts +4 -0
- package/src/utils/logoV2Utils.ts +7 -3
- package/src/utils/messageQueueManager.ts +23 -9
- package/src/utils/model/model.ts +10 -9
- package/src/utils/model/modelOptions.ts +0 -21
- package/src/utils/model/validateModel.ts +0 -19
- package/src/utils/permissions/permissionSetup.ts +5 -0
- package/src/utils/permissions/permissions.ts +25 -0
- package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
- package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
- package/src/utils/permissions/yoloClassifier.ts +28 -6
- package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
- package/src/utils/systemThemeWatcher.ts +62 -0
- package/src/utils/udsClient.ts +115 -0
- package/src/utils/udsMessaging.ts +157 -0
- package/src/utils/yaml.ts +251 -5
- package/src/utils/model/providerModels.ts +0 -168
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
import {
|
|
3
|
+
getStore,
|
|
4
|
+
renderMessageForSummary,
|
|
5
|
+
} from '../../services/contextCollapse/operations.js'
|
|
6
|
+
import { buildTool } from '../../Tool.js'
|
|
7
|
+
import { lazySchema } from '../../utils/lazySchema.js'
|
|
8
|
+
|
|
9
|
+
export const CTX_INSPECT_TOOL_NAME = 'CtxInspect'
|
|
10
|
+
|
|
11
|
+
const DESCRIPTION = `Look inside the context window's collapsed spans. Earlier parts of this conversation may have been replaced by <collapsed id="..."> summaries to save space. Call with no id to list the collapsed spans (id, size, first line of the summary); call with an id to read that span's full summary, or with id and detail "messages" to read the original messages verbatim (large — only when the summary is not enough). Also lists spans that are staged (summarized, not yet swapped in).`
|
|
12
|
+
|
|
13
|
+
const inputSchema = lazySchema(() =>
|
|
14
|
+
z.strictObject({
|
|
15
|
+
id: z.string().optional().describe('Collapse id from a <collapsed id="..."> placeholder'),
|
|
16
|
+
detail: z.enum(['summary', 'messages']).optional().describe('What to return for an id (default summary)'),
|
|
17
|
+
limit: z.number().int().positive().max(200_000).optional().describe('Max characters of original messages to return (default 30000)'),
|
|
18
|
+
}),
|
|
19
|
+
)
|
|
20
|
+
type InputSchema = ReturnType<typeof inputSchema>
|
|
21
|
+
|
|
22
|
+
const outputSchema = lazySchema(() =>
|
|
23
|
+
z.object({
|
|
24
|
+
text: z.string(),
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
type OutputSchema = ReturnType<typeof outputSchema>
|
|
28
|
+
export type Output = z.infer<OutputSchema>
|
|
29
|
+
|
|
30
|
+
export const CtxInspectTool = buildTool({
|
|
31
|
+
name: CTX_INSPECT_TOOL_NAME,
|
|
32
|
+
searchHint: 'read summaries or originals of collapsed conversation spans',
|
|
33
|
+
maxResultSizeChars: 250_000,
|
|
34
|
+
userFacingName: () => 'Context inspect',
|
|
35
|
+
get inputSchema(): InputSchema {
|
|
36
|
+
return inputSchema()
|
|
37
|
+
},
|
|
38
|
+
get outputSchema(): OutputSchema {
|
|
39
|
+
return outputSchema()
|
|
40
|
+
},
|
|
41
|
+
isConcurrencySafe() {
|
|
42
|
+
return true
|
|
43
|
+
},
|
|
44
|
+
isReadOnly() {
|
|
45
|
+
return true
|
|
46
|
+
},
|
|
47
|
+
async checkPermissions(input) {
|
|
48
|
+
return { behavior: 'allow', updatedInput: input }
|
|
49
|
+
},
|
|
50
|
+
async description({ id }) {
|
|
51
|
+
return id ? `Inspect collapsed span ${id}` : 'List collapsed spans'
|
|
52
|
+
},
|
|
53
|
+
async prompt() {
|
|
54
|
+
return DESCRIPTION
|
|
55
|
+
},
|
|
56
|
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
|
57
|
+
return { tool_use_id: toolUseID, type: 'tool_result', content: output.text }
|
|
58
|
+
},
|
|
59
|
+
renderToolUseMessage(input: { id?: string; detail?: string }) {
|
|
60
|
+
return input.id ? `${input.id}${input.detail === 'messages' ? ' (messages)' : ''}` : 'list'
|
|
61
|
+
},
|
|
62
|
+
renderToolResultMessage(output: Output) {
|
|
63
|
+
return output.text.split('\n')[0]?.slice(0, 100) ?? ''
|
|
64
|
+
},
|
|
65
|
+
async call({ id, detail, limit }) {
|
|
66
|
+
const store = getStore()
|
|
67
|
+
if (!id) {
|
|
68
|
+
const lines: string[] = []
|
|
69
|
+
if (store.commits.length === 0 && store.staged.length === 0) {
|
|
70
|
+
lines.push('No collapsed spans yet. The conversation is still fully in context.')
|
|
71
|
+
}
|
|
72
|
+
if (store.commits.length > 0) {
|
|
73
|
+
lines.push(`Collapsed spans (${store.commits.length}):`)
|
|
74
|
+
for (const c of store.commits) {
|
|
75
|
+
const first = c.summary.split('\n').find(l => l.trim()) ?? ''
|
|
76
|
+
lines.push(`- id ${c.collapseId}: ${c.archived.length} messages — ${first.slice(0, 140)}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (store.staged.length > 0) {
|
|
80
|
+
lines.push(`Staged (summarized, not yet swapped in): ${store.staged.length}`)
|
|
81
|
+
for (const s of store.staged) {
|
|
82
|
+
lines.push(`- ${s.startUuid.slice(0, 8)}..${s.endUuid.slice(0, 8)}: ${s.summary.split('\n')[0]?.slice(0, 120) ?? ''}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { data: { text: lines.join('\n') } }
|
|
86
|
+
}
|
|
87
|
+
const commit = store.commits.find(c => c.collapseId === id || c.collapseId === id.padStart(16, '0'))
|
|
88
|
+
if (!commit) {
|
|
89
|
+
return { data: { text: `No collapsed span with id ${id}. Call without an id to list them.` } }
|
|
90
|
+
}
|
|
91
|
+
if (detail === 'messages') {
|
|
92
|
+
if (commit.archived.length === 0) {
|
|
93
|
+
return { data: { text: `The original messages of span ${id} are not loaded in this session (they live in the transcript). Summary:\n\n${commit.summary}` } }
|
|
94
|
+
}
|
|
95
|
+
const max = limit ?? 30_000
|
|
96
|
+
let text = commit.archived.map(renderMessageForSummary).join('\n\n')
|
|
97
|
+
if (text.length > max) text = text.slice(0, max) + `\n… (${text.length - max} more characters)`
|
|
98
|
+
return { data: { text: `Original messages of span ${id} (${commit.archived.length}):\n\n${text}` } }
|
|
99
|
+
}
|
|
100
|
+
return { data: { text: `Summary of span ${id} (${commit.archived.length} messages):\n\n${commit.summary}` } }
|
|
101
|
+
},
|
|
102
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
import type { Command } from '../../commands.js'
|
|
3
|
+
import { searchSkills } from '../../services/skillSearch/localSearch.js'
|
|
4
|
+
import { buildTool } from '../../Tool.js'
|
|
5
|
+
import { lazySchema } from '../../utils/lazySchema.js'
|
|
6
|
+
import { DESCRIPTION, DISCOVER_SKILLS_TOOL_NAME } from './prompt.js'
|
|
7
|
+
|
|
8
|
+
const inputSchema = lazySchema(() =>
|
|
9
|
+
z.strictObject({
|
|
10
|
+
description: z.string().describe('What you are about to do, specifically'),
|
|
11
|
+
limit: z.number().int().positive().max(10).optional().describe('Max results (default 5)'),
|
|
12
|
+
}),
|
|
13
|
+
)
|
|
14
|
+
type InputSchema = ReturnType<typeof inputSchema>
|
|
15
|
+
|
|
16
|
+
const outputSchema = lazySchema(() =>
|
|
17
|
+
z.object({
|
|
18
|
+
skills: z.array(z.object({ name: z.string(), description: z.string() })),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
type OutputSchema = ReturnType<typeof outputSchema>
|
|
23
|
+
export type Output = z.infer<OutputSchema>
|
|
24
|
+
|
|
25
|
+
export const DiscoverSkillsTool = buildTool({
|
|
26
|
+
name: DISCOVER_SKILLS_TOOL_NAME,
|
|
27
|
+
searchHint: 'find installed skills relevant to a task',
|
|
28
|
+
maxResultSizeChars: 20_000,
|
|
29
|
+
userFacingName: () => 'Discover skills',
|
|
30
|
+
get inputSchema(): InputSchema {
|
|
31
|
+
return inputSchema()
|
|
32
|
+
},
|
|
33
|
+
get outputSchema(): OutputSchema {
|
|
34
|
+
return outputSchema()
|
|
35
|
+
},
|
|
36
|
+
isConcurrencySafe() {
|
|
37
|
+
return true
|
|
38
|
+
},
|
|
39
|
+
isReadOnly() {
|
|
40
|
+
return true
|
|
41
|
+
},
|
|
42
|
+
async checkPermissions(input) {
|
|
43
|
+
return { behavior: 'allow', updatedInput: input }
|
|
44
|
+
},
|
|
45
|
+
async description({ description }) {
|
|
46
|
+
return `Find skills for: ${description.slice(0, 60)}`
|
|
47
|
+
},
|
|
48
|
+
async prompt() {
|
|
49
|
+
return DESCRIPTION
|
|
50
|
+
},
|
|
51
|
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
|
52
|
+
return {
|
|
53
|
+
tool_use_id: toolUseID,
|
|
54
|
+
type: 'tool_result',
|
|
55
|
+
content:
|
|
56
|
+
output.skills.length === 0
|
|
57
|
+
? output.message
|
|
58
|
+
: output.skills.map(s => `- ${s.name}: ${s.description}`).join('\n') + `\n\nInvoke with Skill("<name>").`,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
renderToolUseMessage(input: { description?: string }) {
|
|
62
|
+
return input.description ?? ''
|
|
63
|
+
},
|
|
64
|
+
renderToolResultMessage(output: Output) {
|
|
65
|
+
return output.skills.length ? output.skills.map(s => s.name).join(', ') : 'no matching skills'
|
|
66
|
+
},
|
|
67
|
+
async call({ description, limit }, context) {
|
|
68
|
+
const commands = ((context.options as { commands?: Command[] } | undefined)?.commands ?? []).filter(c => c.type === 'prompt')
|
|
69
|
+
const hits = searchSkills(description, commands, { limit: limit ?? 5, minScore: 1.5 })
|
|
70
|
+
for (const h of hits) context.discoveredSkillNames?.add(h.name)
|
|
71
|
+
return {
|
|
72
|
+
data: {
|
|
73
|
+
skills: hits.map(h => ({ name: h.name, description: h.description })),
|
|
74
|
+
message: hits.length ? `${hits.length} skills match.` : 'No installed skill matches that description.',
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
})
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const DISCOVER_SKILLS_TOOL_NAME = 'DiscoverSkills'
|
|
2
|
+
|
|
3
|
+
export const DESCRIPTION = `Find installed skills relevant to what you are about to do. Relevant skills are surfaced automatically each turn as "Skills relevant to your task" reminders; call this when you are starting something those did not cover (a mid-task pivot, an unusual workflow, a multi-step plan) with a specific description of the work. Returns skill names and descriptions; invoke one with Skill("<name>").`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
import { buildTool } from '../../Tool.js'
|
|
3
|
+
import { lazySchema } from '../../utils/lazySchema.js'
|
|
4
|
+
import { listAllLiveSessions, type LiveSession } from '../../utils/udsClient.js'
|
|
5
|
+
|
|
6
|
+
export const LIST_PEERS_TOOL_NAME = 'ListPeers'
|
|
7
|
+
|
|
8
|
+
const DESCRIPTION = `List the other Xela sessions running on this machine that can receive messages. Each peer has a socket address; send to it with SendMessage({ to: "uds:<socket path>", message: "..." }). The message arrives in that session as a user prompt wrapped in <cross-session-message from="uds:...">; the peer replies by sending to the from address. Use it to coordinate work across projects or hand a question to a session that has the right context.`
|
|
9
|
+
|
|
10
|
+
const inputSchema = lazySchema(() => z.strictObject({}))
|
|
11
|
+
type InputSchema = ReturnType<typeof inputSchema>
|
|
12
|
+
|
|
13
|
+
const outputSchema = lazySchema(() =>
|
|
14
|
+
z.object({
|
|
15
|
+
peers: z.array(
|
|
16
|
+
z.object({
|
|
17
|
+
address: z.string(),
|
|
18
|
+
pid: z.number(),
|
|
19
|
+
cwd: z.string().optional(),
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
kind: z.string().optional(),
|
|
22
|
+
startedAt: z.number().optional(),
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
self: z.string().optional(),
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
type OutputSchema = ReturnType<typeof outputSchema>
|
|
29
|
+
export type Output = z.infer<OutputSchema>
|
|
30
|
+
|
|
31
|
+
export const ListPeersTool = buildTool({
|
|
32
|
+
name: LIST_PEERS_TOOL_NAME,
|
|
33
|
+
searchHint: 'discover other local sessions to message',
|
|
34
|
+
maxResultSizeChars: 20_000,
|
|
35
|
+
userFacingName: () => 'List peers',
|
|
36
|
+
get inputSchema(): InputSchema {
|
|
37
|
+
return inputSchema()
|
|
38
|
+
},
|
|
39
|
+
get outputSchema(): OutputSchema {
|
|
40
|
+
return outputSchema()
|
|
41
|
+
},
|
|
42
|
+
isConcurrencySafe() {
|
|
43
|
+
return true
|
|
44
|
+
},
|
|
45
|
+
isReadOnly() {
|
|
46
|
+
return true
|
|
47
|
+
},
|
|
48
|
+
async checkPermissions(input) {
|
|
49
|
+
return { behavior: 'allow', updatedInput: input }
|
|
50
|
+
},
|
|
51
|
+
async description() {
|
|
52
|
+
return 'List other local sessions'
|
|
53
|
+
},
|
|
54
|
+
async prompt() {
|
|
55
|
+
return DESCRIPTION
|
|
56
|
+
},
|
|
57
|
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
|
58
|
+
const lines =
|
|
59
|
+
output.peers.length === 0
|
|
60
|
+
? ['No other reachable sessions on this machine.']
|
|
61
|
+
: output.peers.map(
|
|
62
|
+
p =>
|
|
63
|
+
`- ${p.address} pid ${p.pid}${p.name ? ` name ${p.name}` : ''}${p.cwd ? ` cwd ${p.cwd}` : ''}${p.kind && p.kind !== 'interactive' ? ` (${p.kind})` : ''}`,
|
|
64
|
+
)
|
|
65
|
+
if (output.self) lines.push(`(this session: uds:${output.self})`)
|
|
66
|
+
return { tool_use_id: toolUseID, type: 'tool_result', content: lines.join('\n') }
|
|
67
|
+
},
|
|
68
|
+
renderToolUseMessage() {
|
|
69
|
+
return ''
|
|
70
|
+
},
|
|
71
|
+
renderToolResultMessage(output: Output) {
|
|
72
|
+
return `${output.peers.length} peer${output.peers.length === 1 ? '' : 's'}`
|
|
73
|
+
},
|
|
74
|
+
async call() {
|
|
75
|
+
const sessions = await listAllLiveSessions()
|
|
76
|
+
const self = sessions.find(s => s.isSelf)
|
|
77
|
+
const peers = sessions
|
|
78
|
+
.filter((s): s is LiveSession & { messagingSocketPath: string } => !s.isSelf && s.reachable && !!s.messagingSocketPath)
|
|
79
|
+
.map(s => ({
|
|
80
|
+
address: `uds:${s.messagingSocketPath}`,
|
|
81
|
+
pid: s.pid,
|
|
82
|
+
cwd: s.cwd,
|
|
83
|
+
name: s.name,
|
|
84
|
+
kind: s.kind,
|
|
85
|
+
startedAt: s.startedAt,
|
|
86
|
+
}))
|
|
87
|
+
return { data: { peers, self: self?.messagingSocketPath } }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
@@ -59,6 +59,12 @@ export const MCPTool = buildTool({
|
|
|
59
59
|
message: 'MCPTool requires permission.',
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
|
+
// MCP tools can do anything (context-mode's ctx_execute runs shell commands),
|
|
63
|
+
// so auto mode must classify them instead of skipping them as irrelevant
|
|
64
|
+
toAutoClassifierInput(input) {
|
|
65
|
+
const text = JSON.stringify(input)
|
|
66
|
+
return text.length > 4000 ? text.slice(0, 4000) + '…' : text
|
|
67
|
+
},
|
|
62
68
|
renderToolUseMessage,
|
|
63
69
|
// Overridden in mcpClient.ts
|
|
64
70
|
userFacingName: () => 'mcp',
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
import { spawnShellTask } from '../../tasks/LocalShellTask/LocalShellTask.js'
|
|
3
|
+
import { killTask } from '../../tasks/LocalShellTask/killShellTasks.js'
|
|
4
|
+
import { spawnMonitorMcpTask } from '../../tasks/MonitorMcpTask/MonitorMcpTask.js'
|
|
5
|
+
import { buildTool } from '../../Tool.js'
|
|
6
|
+
import type { AgentId } from '../../types/ids.js'
|
|
7
|
+
import { lazySchema } from '../../utils/lazySchema.js'
|
|
8
|
+
import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'
|
|
9
|
+
import { exec } from '../../utils/Shell.js'
|
|
10
|
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
|
11
|
+
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
|
12
|
+
import { TaskOutput } from '../../utils/task/TaskOutput.js'
|
|
13
|
+
import { tailFile } from '../../utils/fsOperations.js'
|
|
14
|
+
import { bashToolHasPermission } from '../BashTool/bashPermissions.js'
|
|
15
|
+
import { createEventEmitter, MAX_EVENTS_PER_MINUTE } from './events.js'
|
|
16
|
+
import { DESCRIPTION, MONITOR_TOOL_NAME } from './prompt.js'
|
|
17
|
+
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000
|
|
20
|
+
const MAX_TIMEOUT_MS = 30 * 60 * 1000
|
|
21
|
+
const inputSchema = lazySchema(() =>
|
|
22
|
+
z.strictObject({
|
|
23
|
+
command: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(
|
|
27
|
+
'Shell command or script. Each stdout line is an event; exit ends the watch.',
|
|
28
|
+
),
|
|
29
|
+
ws: z
|
|
30
|
+
.object({
|
|
31
|
+
url: z.string(),
|
|
32
|
+
protocols: z.array(z.string()).optional(),
|
|
33
|
+
})
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(
|
|
36
|
+
'WebSocket to open. Each text frame is an event; socket close ends the watch. Cannot be combined with command.',
|
|
37
|
+
),
|
|
38
|
+
description: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(
|
|
41
|
+
'Short human-readable description of what you are monitoring (shown in notifications).',
|
|
42
|
+
),
|
|
43
|
+
timeout_ms: z
|
|
44
|
+
.number()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe(
|
|
47
|
+
'Kill the monitor after this deadline. Default 300000ms, at most 1800000ms. You are notified at expiry and can re-arm.',
|
|
48
|
+
),
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
type InputSchema = ReturnType<typeof inputSchema>
|
|
52
|
+
export type MonitorInput = z.infer<InputSchema>
|
|
53
|
+
|
|
54
|
+
const outputSchema = lazySchema(() =>
|
|
55
|
+
z.object({
|
|
56
|
+
taskId: z.string(),
|
|
57
|
+
description: z.string(),
|
|
58
|
+
source: z.enum(['command', 'ws']),
|
|
59
|
+
outputFile: z.string(),
|
|
60
|
+
timeoutMs: z.number(),
|
|
61
|
+
message: z.string(),
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
type OutputSchema = ReturnType<typeof outputSchema>
|
|
65
|
+
export type Output = z.infer<OutputSchema>
|
|
66
|
+
|
|
67
|
+
export function clampTimeout(timeoutMs: number | undefined): number {
|
|
68
|
+
if (!timeoutMs || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
69
|
+
return DEFAULT_TIMEOUT_MS
|
|
70
|
+
}
|
|
71
|
+
return Math.min(timeoutMs, MAX_TIMEOUT_MS)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const MonitorTool = buildTool({
|
|
75
|
+
name: MONITOR_TOOL_NAME,
|
|
76
|
+
searchHint: 'watch a log, process or socket and get notified on each event',
|
|
77
|
+
maxResultSizeChars: 100_000,
|
|
78
|
+
userFacingName: () => 'Monitor',
|
|
79
|
+
get inputSchema(): InputSchema {
|
|
80
|
+
return inputSchema()
|
|
81
|
+
},
|
|
82
|
+
get outputSchema(): OutputSchema {
|
|
83
|
+
return outputSchema()
|
|
84
|
+
},
|
|
85
|
+
isConcurrencySafe() {
|
|
86
|
+
return true
|
|
87
|
+
},
|
|
88
|
+
isReadOnly() {
|
|
89
|
+
return false
|
|
90
|
+
},
|
|
91
|
+
toAutoClassifierInput(input) {
|
|
92
|
+
return input.command ?? (input.ws ? `ws ${input.ws.url}` : '')
|
|
93
|
+
},
|
|
94
|
+
async validateInput({ command, ws, timeout_ms }) {
|
|
95
|
+
if (!command && !ws) {
|
|
96
|
+
return {
|
|
97
|
+
result: false,
|
|
98
|
+
message: 'Provide either command or ws',
|
|
99
|
+
errorCode: 1,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (command && ws) {
|
|
103
|
+
return {
|
|
104
|
+
result: false,
|
|
105
|
+
message: 'command and ws cannot be combined',
|
|
106
|
+
errorCode: 1,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (ws && !/^wss?:\/\//.test(ws.url)) {
|
|
110
|
+
return {
|
|
111
|
+
result: false,
|
|
112
|
+
message: 'ws.url must start with ws:// or wss://',
|
|
113
|
+
errorCode: 1,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (timeout_ms !== undefined && timeout_ms < 1000) {
|
|
117
|
+
return {
|
|
118
|
+
result: false,
|
|
119
|
+
message: 'timeout_ms must be at least 1000',
|
|
120
|
+
errorCode: 1,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { result: true }
|
|
124
|
+
},
|
|
125
|
+
async checkPermissions(input, context) {
|
|
126
|
+
// A shell monitor is a Bash command: same rules, same prompts
|
|
127
|
+
if (input.command) {
|
|
128
|
+
const result = await bashToolHasPermission(
|
|
129
|
+
{ command: input.command, description: input.description },
|
|
130
|
+
context,
|
|
131
|
+
)
|
|
132
|
+
// Pass the monitor input through, not the Bash-shaped input
|
|
133
|
+
if (result.behavior === 'allow') {
|
|
134
|
+
return { ...result, updatedInput: input }
|
|
135
|
+
}
|
|
136
|
+
return result
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
behavior: 'ask',
|
|
140
|
+
message: `Open WebSocket ${input.ws?.url ?? ''}`,
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
async description({ description }) {
|
|
144
|
+
return description
|
|
145
|
+
},
|
|
146
|
+
async prompt() {
|
|
147
|
+
return DESCRIPTION
|
|
148
|
+
},
|
|
149
|
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
|
150
|
+
return {
|
|
151
|
+
tool_use_id: toolUseID,
|
|
152
|
+
type: 'tool_result',
|
|
153
|
+
content: jsonStringify(output),
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
renderToolUseMessage,
|
|
157
|
+
renderToolResultMessage,
|
|
158
|
+
async call(input, context) {
|
|
159
|
+
const { abortController, getAppState, setAppState } = context
|
|
160
|
+
const toolUseId = (context as { toolUseId?: string }).toolUseId
|
|
161
|
+
const agentId = (context as { agentId?: AgentId }).agentId
|
|
162
|
+
const timeoutMs = clampTimeout(input.timeout_ms)
|
|
163
|
+
const description = input.description
|
|
164
|
+
|
|
165
|
+
if (input.ws) {
|
|
166
|
+
const handle = await spawnMonitorMcpTask(
|
|
167
|
+
{
|
|
168
|
+
url: input.ws.url,
|
|
169
|
+
protocols: input.ws.protocols,
|
|
170
|
+
description,
|
|
171
|
+
timeoutMs,
|
|
172
|
+
toolUseId,
|
|
173
|
+
agentId,
|
|
174
|
+
},
|
|
175
|
+
{ abortController, getAppState, setAppState },
|
|
176
|
+
)
|
|
177
|
+
return {
|
|
178
|
+
data: {
|
|
179
|
+
taskId: handle.taskId,
|
|
180
|
+
description,
|
|
181
|
+
source: 'ws' as const,
|
|
182
|
+
outputFile: getTaskOutputPath(handle.taskId),
|
|
183
|
+
timeoutMs,
|
|
184
|
+
message: `Monitor "${description}" armed on ${input.ws.url} (task ${handle.taskId}, expires in ${Math.round(timeoutMs / 1000)}s). Each text frame arrives as a notification.`,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const command = input.command!
|
|
190
|
+
let emitter: ReturnType<typeof createEventEmitter> | null = null
|
|
191
|
+
let seenLines = 0
|
|
192
|
+
let taskId: string | null = null
|
|
193
|
+
|
|
194
|
+
// The poller hands over the last ~100 lines plus the total line count, so
|
|
195
|
+
// new lines are the tail slice beyond what was already delivered.
|
|
196
|
+
function deliver(content: string, totalLines: number): void {
|
|
197
|
+
if (!emitter || totalLines <= seenLines) return
|
|
198
|
+
const lines = content.split('\n')
|
|
199
|
+
if (lines[lines.length - 1] === '') lines.pop()
|
|
200
|
+
const fresh = lines.slice(Math.max(0, lines.length - (totalLines - seenLines)))
|
|
201
|
+
seenLines = totalLines
|
|
202
|
+
for (const line of fresh) {
|
|
203
|
+
if (line.trim() === '') continue
|
|
204
|
+
if (!emitter.push(line)) break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const shellCommand = await exec(command, abortController.signal, 'bash', {
|
|
209
|
+
timeout: timeoutMs,
|
|
210
|
+
onProgress(_lastLines, allLines, totalLines) {
|
|
211
|
+
if (process.env.XELA_MONITOR_DEBUG) process.stderr.write(`[monitor] progress total=${totalLines} seen=${seenLines}\n`)
|
|
212
|
+
deliver(allLines, totalLines)
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const handle = await spawnShellTask(
|
|
217
|
+
{
|
|
218
|
+
command,
|
|
219
|
+
description,
|
|
220
|
+
shellCommand,
|
|
221
|
+
toolUseId,
|
|
222
|
+
agentId,
|
|
223
|
+
kind: 'monitor',
|
|
224
|
+
timeout: timeoutMs,
|
|
225
|
+
},
|
|
226
|
+
{ abortController, getAppState, setAppState },
|
|
227
|
+
)
|
|
228
|
+
taskId = handle.taskId
|
|
229
|
+
emitter = createEventEmitter({
|
|
230
|
+
taskId,
|
|
231
|
+
description,
|
|
232
|
+
toolUseId,
|
|
233
|
+
agentId,
|
|
234
|
+
onFlood: () => {
|
|
235
|
+
enqueuePendingNotification({
|
|
236
|
+
value: `Monitor "${description}" (task ${taskId}) produced more than ${MAX_EVENTS_PER_MINUTE} events per minute and was stopped. Restart it with a tighter filter.`,
|
|
237
|
+
mode: 'task-notification',
|
|
238
|
+
priority: 'next',
|
|
239
|
+
agentId,
|
|
240
|
+
})
|
|
241
|
+
if (taskId) killTask(taskId, setAppState)
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Progress callbacks only fire while the shared poller watches this task
|
|
246
|
+
const outputId = shellCommand.taskOutput.taskId
|
|
247
|
+
TaskOutput.startPolling(outputId)
|
|
248
|
+
void shellCommand.result.then(async () => {
|
|
249
|
+
TaskOutput.stopPolling(outputId)
|
|
250
|
+
// The script may exit between two polls: deliver whatever the last
|
|
251
|
+
// tick missed, then flush so the events precede the "stream ended" notice
|
|
252
|
+
try {
|
|
253
|
+
const { content } = await tailFile(getTaskOutputPath(outputId), 256 * 1024)
|
|
254
|
+
const total = content.split('\n').filter((l, i, a) => !(i === a.length - 1 && l === '')).length
|
|
255
|
+
deliver(content, Math.max(total, seenLines))
|
|
256
|
+
} catch {
|
|
257
|
+
// output file may be gone already
|
|
258
|
+
}
|
|
259
|
+
emitter?.flush()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
data: {
|
|
264
|
+
taskId,
|
|
265
|
+
description,
|
|
266
|
+
source: 'command' as const,
|
|
267
|
+
outputFile: getTaskOutputPath(taskId),
|
|
268
|
+
timeoutMs,
|
|
269
|
+
message: `Monitor "${description}" armed (task ${taskId}, expires in ${Math.round(timeoutMs / 1000)}s). Each stdout line arrives as a notification; the script exiting ends the watch.`,
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { MessageResponse } from '../../components/MessageResponse.js'
|
|
3
|
+
import { Text } from '../../ink.js'
|
|
4
|
+
import type { MonitorInput, Output } from './MonitorTool.js'
|
|
5
|
+
|
|
6
|
+
export function renderToolUseMessage(
|
|
7
|
+
input: Partial<MonitorInput>,
|
|
8
|
+
{ verbose }: { verbose: boolean },
|
|
9
|
+
): React.ReactNode {
|
|
10
|
+
const source = input.command ?? input.ws?.url ?? ''
|
|
11
|
+
const shown =
|
|
12
|
+
verbose || source.length <= 80 ? source : source.slice(0, 77) + '…'
|
|
13
|
+
return input.description ? `${input.description} · ${shown}` : shown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderToolResultMessage(output: Output): React.ReactNode {
|
|
17
|
+
return (
|
|
18
|
+
<MessageResponse>
|
|
19
|
+
<Text>
|
|
20
|
+
Monitor armed · {output.description} · task {output.taskId} · expires
|
|
21
|
+
in {Math.round(output.timeoutMs / 1000)}s
|
|
22
|
+
</Text>
|
|
23
|
+
</MessageResponse>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OUTPUT_FILE_TAG,
|
|
3
|
+
SUMMARY_TAG,
|
|
4
|
+
TASK_ID_TAG,
|
|
5
|
+
TASK_NOTIFICATION_TAG,
|
|
6
|
+
TOOL_USE_ID_TAG,
|
|
7
|
+
} from '../../constants/xml.js'
|
|
8
|
+
import type { AgentId } from '../../types/ids.js'
|
|
9
|
+
import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'
|
|
10
|
+
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
|
11
|
+
import { escapeXml } from '../../utils/xml.js'
|
|
12
|
+
|
|
13
|
+
// Lines arriving within this window are delivered as one notification
|
|
14
|
+
export const EVENT_BATCH_MS = 200
|
|
15
|
+
// A monitor emitting more than this many notifications per minute is stopped:
|
|
16
|
+
// the filter is too wide and would flood the conversation
|
|
17
|
+
export const MAX_EVENTS_PER_MINUTE = 30
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds the batching event emitter shared by the shell and WebSocket
|
|
21
|
+
* monitors. Lines pushed within EVENT_BATCH_MS become one task notification
|
|
22
|
+
* (no <status> tag, so the task stays open). Returns `false` from push() once
|
|
23
|
+
* the rate limit tripped, so the caller can stop the source.
|
|
24
|
+
*/
|
|
25
|
+
export function createEventEmitter(opts: {
|
|
26
|
+
taskId: string
|
|
27
|
+
description: string
|
|
28
|
+
toolUseId?: string
|
|
29
|
+
agentId?: AgentId
|
|
30
|
+
onFlood: () => void
|
|
31
|
+
}): { push: (line: string) => boolean; flush: () => void } {
|
|
32
|
+
const { taskId, description, toolUseId, agentId, onFlood } = opts
|
|
33
|
+
const outputPath = getTaskOutputPath(taskId)
|
|
34
|
+
const toolUseIdLine = toolUseId
|
|
35
|
+
? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>`
|
|
36
|
+
: ''
|
|
37
|
+
let pending: string[] = []
|
|
38
|
+
let timer: NodeJS.Timeout | null = null
|
|
39
|
+
let flooded = false
|
|
40
|
+
const recent: number[] = []
|
|
41
|
+
|
|
42
|
+
function flush(): void {
|
|
43
|
+
timer = null
|
|
44
|
+
if (pending.length === 0) return
|
|
45
|
+
const lines = pending
|
|
46
|
+
pending = []
|
|
47
|
+
const now = Date.now()
|
|
48
|
+
recent.push(now)
|
|
49
|
+
while (recent.length > 0 && now - recent[0]! > 60_000) recent.shift()
|
|
50
|
+
if (recent.length > MAX_EVENTS_PER_MINUTE && !flooded) {
|
|
51
|
+
flooded = true
|
|
52
|
+
onFlood()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
const summary = `Monitor "${description}": ${lines.length === 1 ? '1 event' : `${lines.length} events`}`
|
|
56
|
+
const message = `<${TASK_NOTIFICATION_TAG}>
|
|
57
|
+
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
|
|
58
|
+
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
|
|
59
|
+
<${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}>
|
|
60
|
+
</${TASK_NOTIFICATION_TAG}>
|
|
61
|
+
${lines.join('\n')}`
|
|
62
|
+
if (process.env.XELA_MONITOR_DEBUG) process.stderr.write(`[monitor] flush ${lines.length} lines -> notification\n`)
|
|
63
|
+
enqueuePendingNotification({
|
|
64
|
+
value: message,
|
|
65
|
+
mode: 'task-notification',
|
|
66
|
+
priority: 'next',
|
|
67
|
+
agentId,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
push(line: string): boolean {
|
|
73
|
+
if (flooded) return false
|
|
74
|
+
pending.push(line)
|
|
75
|
+
if (!timer) {
|
|
76
|
+
timer = setTimeout(flush, EVENT_BATCH_MS)
|
|
77
|
+
timer.unref()
|
|
78
|
+
}
|
|
79
|
+
return true
|
|
80
|
+
},
|
|
81
|
+
flush,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|