@xortex/xcode 3.1.2 → 3.1.3

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.
@@ -0,0 +1,98 @@
1
+ import memoize from 'lodash-es/memoize.js'
2
+ import { basename } from 'path'
3
+ import type { OutputStyleConfig } from '../constants/outputStyles.js'
4
+ import { logForDebugging } from '../utils/debug.js'
5
+ import { coerceDescriptionToString } from '../utils/frontmatterParser.js'
6
+ import { logError } from '../utils/log.js'
7
+ import {
8
+ extractDescriptionFromMarkdown,
9
+ loadMarkdownFilesForSubdir,
10
+ } from '../utils/markdownConfigLoader.js'
11
+ import { clearPluginOutputStyleCache } from '../utils/plugins/loadPluginOutputStyles.js'
12
+
13
+ /**
14
+ * Loads markdown files from .claude/output-styles directories throughout the project
15
+ * and from ~/.claude/output-styles directory and converts them to output styles.
16
+ *
17
+ * Each filename becomes a style name, and the file content becomes the style prompt.
18
+ * The frontmatter provides name and description.
19
+ *
20
+ * Structure:
21
+ * - Project .claude/output-styles/*.md -> project styles
22
+ * - User ~/.claude/output-styles/*.md -> user styles (overridden by project styles)
23
+ *
24
+ * @param cwd Current working directory for project directory traversal
25
+ */
26
+ export const getOutputStyleDirStyles = memoize(
27
+ async (cwd: string): Promise<OutputStyleConfig[]> => {
28
+ try {
29
+ const markdownFiles = await loadMarkdownFilesForSubdir(
30
+ 'output-styles',
31
+ cwd,
32
+ )
33
+
34
+ const styles = markdownFiles
35
+ .map(({ filePath, frontmatter, content, source }) => {
36
+ try {
37
+ const fileName = basename(filePath)
38
+ const styleName = fileName.replace(/\.md$/, '')
39
+
40
+ // Get style configuration from frontmatter
41
+ const name = (frontmatter['name'] || styleName) as string
42
+ const description =
43
+ coerceDescriptionToString(
44
+ frontmatter['description'],
45
+ styleName,
46
+ ) ??
47
+ extractDescriptionFromMarkdown(
48
+ content,
49
+ `Custom ${styleName} output style`,
50
+ )
51
+
52
+ // Parse keep-coding-instructions flag (supports both boolean and string values)
53
+ const keepCodingInstructionsRaw =
54
+ frontmatter['keep-coding-instructions']
55
+ const keepCodingInstructions =
56
+ keepCodingInstructionsRaw === true ||
57
+ keepCodingInstructionsRaw === 'true'
58
+ ? true
59
+ : keepCodingInstructionsRaw === false ||
60
+ keepCodingInstructionsRaw === 'false'
61
+ ? false
62
+ : undefined
63
+
64
+ // Warn if force-for-plugin is set on non-plugin output style
65
+ if (frontmatter['force-for-plugin'] !== undefined) {
66
+ logForDebugging(
67
+ `Output style "${name}" has force-for-plugin set, but this option only applies to plugin output styles. Ignoring.`,
68
+ { level: 'warn' },
69
+ )
70
+ }
71
+
72
+ return {
73
+ name,
74
+ description,
75
+ prompt: content.trim(),
76
+ source,
77
+ keepCodingInstructions,
78
+ }
79
+ } catch (error) {
80
+ logError(error)
81
+ return null
82
+ }
83
+ })
84
+ .filter(style => style !== null)
85
+
86
+ return styles
87
+ } catch (error) {
88
+ logError(error)
89
+ return []
90
+ }
91
+ },
92
+ )
93
+
94
+ export function clearOutputStyleCaches(): void {
95
+ getOutputStyleDirStyles.cache?.clear?.()
96
+ loadMarkdownFilesForSubdir.cache?.clear?.()
97
+ clearPluginOutputStyleCache()
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xortex/xcode",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "XCode - AI-powered coding assistant with XMem long-term memory. Supports Claude, Gemini, Kimi, DeepSeek via OpenRouter.",
5
5
  "main": "main.tsx",
6
6
  "bin": {
@@ -37,7 +37,14 @@
37
37
  "types/",
38
38
  "upstreamproxy/",
39
39
  "utils/",
40
+ "vim/",
40
41
  "voice/",
42
+ "plugins/",
43
+ "public/",
44
+ "server/",
45
+ "outputStyles/",
46
+ "moreright/",
47
+ "native-ts/",
41
48
  "bun-bundle-hook.js",
42
49
  "bun-bundle-shim.ts",
43
50
  "tsconfig.json",
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Built-in Plugin Registry
3
+ *
4
+ * Manages built-in plugins that ship with the CLI and can be enabled/disabled
5
+ * by users via the /plugin UI.
6
+ *
7
+ * Built-in plugins differ from bundled skills (src/skills/bundled/) in that:
8
+ * - They appear in the /plugin UI under a "Built-in" section
9
+ * - Users can enable/disable them (persisted to user settings)
10
+ * - They can provide multiple components (skills, hooks, MCP servers)
11
+ *
12
+ * Plugin IDs use the format `{name}@builtin` to distinguish them from
13
+ * marketplace plugins (`{name}@{marketplace}`).
14
+ */
15
+
16
+ import type { Command } from '../commands.js'
17
+ import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
18
+ import type { BuiltinPluginDefinition, LoadedPlugin } from '../types/plugin.js'
19
+ import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
20
+
21
+ const BUILTIN_PLUGINS: Map<string, BuiltinPluginDefinition> = new Map()
22
+
23
+ export const BUILTIN_MARKETPLACE_NAME = 'builtin'
24
+
25
+ /**
26
+ * Register a built-in plugin. Call this from initBuiltinPlugins() at startup.
27
+ */
28
+ export function registerBuiltinPlugin(
29
+ definition: BuiltinPluginDefinition,
30
+ ): void {
31
+ BUILTIN_PLUGINS.set(definition.name, definition)
32
+ }
33
+
34
+ /**
35
+ * Check if a plugin ID represents a built-in plugin (ends with @builtin).
36
+ */
37
+ export function isBuiltinPluginId(pluginId: string): boolean {
38
+ return pluginId.endsWith(`@${BUILTIN_MARKETPLACE_NAME}`)
39
+ }
40
+
41
+ /**
42
+ * Get a specific built-in plugin definition by name.
43
+ * Useful for the /plugin UI to show the skills/hooks/MCP list without
44
+ * a marketplace lookup.
45
+ */
46
+ export function getBuiltinPluginDefinition(
47
+ name: string,
48
+ ): BuiltinPluginDefinition | undefined {
49
+ return BUILTIN_PLUGINS.get(name)
50
+ }
51
+
52
+ /**
53
+ * Get all registered built-in plugins as LoadedPlugin objects, split into
54
+ * enabled/disabled based on user settings (with defaultEnabled as fallback).
55
+ * Plugins whose isAvailable() returns false are omitted entirely.
56
+ */
57
+ export function getBuiltinPlugins(): {
58
+ enabled: LoadedPlugin[]
59
+ disabled: LoadedPlugin[]
60
+ } {
61
+ const settings = getSettings_DEPRECATED()
62
+ const enabled: LoadedPlugin[] = []
63
+ const disabled: LoadedPlugin[] = []
64
+
65
+ for (const [name, definition] of BUILTIN_PLUGINS) {
66
+ if (definition.isAvailable && !definition.isAvailable()) {
67
+ continue
68
+ }
69
+
70
+ const pluginId = `${name}@${BUILTIN_MARKETPLACE_NAME}`
71
+ const userSetting = settings?.enabledPlugins?.[pluginId]
72
+ // Enabled state: user preference > plugin default > true
73
+ const isEnabled =
74
+ userSetting !== undefined
75
+ ? userSetting === true
76
+ : (definition.defaultEnabled ?? true)
77
+
78
+ const plugin: LoadedPlugin = {
79
+ name,
80
+ manifest: {
81
+ name,
82
+ description: definition.description,
83
+ version: definition.version,
84
+ },
85
+ path: BUILTIN_MARKETPLACE_NAME, // sentinel — no filesystem path
86
+ source: pluginId,
87
+ repository: pluginId,
88
+ enabled: isEnabled,
89
+ isBuiltin: true,
90
+ hooksConfig: definition.hooks,
91
+ mcpServers: definition.mcpServers,
92
+ }
93
+
94
+ if (isEnabled) {
95
+ enabled.push(plugin)
96
+ } else {
97
+ disabled.push(plugin)
98
+ }
99
+ }
100
+
101
+ return { enabled, disabled }
102
+ }
103
+
104
+ /**
105
+ * Get skills from enabled built-in plugins as Command objects.
106
+ * Skills from disabled plugins are not returned.
107
+ */
108
+ export function getBuiltinPluginSkillCommands(): Command[] {
109
+ const { enabled } = getBuiltinPlugins()
110
+ const commands: Command[] = []
111
+
112
+ for (const plugin of enabled) {
113
+ const definition = BUILTIN_PLUGINS.get(plugin.name)
114
+ if (!definition?.skills) continue
115
+ for (const skill of definition.skills) {
116
+ commands.push(skillDefinitionToCommand(skill))
117
+ }
118
+ }
119
+
120
+ return commands
121
+ }
122
+
123
+ /**
124
+ * Clear built-in plugins registry (for testing).
125
+ */
126
+ export function clearBuiltinPlugins(): void {
127
+ BUILTIN_PLUGINS.clear()
128
+ }
129
+
130
+ // --
131
+
132
+ function skillDefinitionToCommand(definition: BundledSkillDefinition): Command {
133
+ return {
134
+ type: 'prompt',
135
+ name: definition.name,
136
+ description: definition.description,
137
+ hasUserSpecifiedDescription: true,
138
+ allowedTools: definition.allowedTools ?? [],
139
+ argumentHint: definition.argumentHint,
140
+ whenToUse: definition.whenToUse,
141
+ model: definition.model,
142
+ disableModelInvocation: definition.disableModelInvocation ?? false,
143
+ userInvocable: definition.userInvocable ?? true,
144
+ contentLength: 0,
145
+ // 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded
146
+ // slash commands (/help, /clear). Using 'bundled' keeps these skills in
147
+ // the Skill tool's listing, analytics name logging, and prompt-truncation
148
+ // exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin.
149
+ source: 'bundled',
150
+ loadedFrom: 'bundled',
151
+ hooks: definition.hooks,
152
+ context: definition.context,
153
+ agent: definition.agent,
154
+ isEnabled: definition.isEnabled ?? (() => true),
155
+ isHidden: !(definition.userInvocable ?? true),
156
+ progressMessage: 'running',
157
+ getPromptForCommand: definition.getPromptForCommand,
158
+ }
159
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Built-in Plugin Initialization
3
+ *
4
+ * Initializes built-in plugins that ship with the CLI and appear in the
5
+ * /plugin UI for users to enable/disable.
6
+ *
7
+ * Not all bundled features should be built-in plugins — use this for
8
+ * features that users should be able to explicitly enable/disable. For
9
+ * features with complex setup or automatic-enabling logic (e.g.
10
+ * claude-in-chrome), use src/skills/bundled/ instead.
11
+ *
12
+ * To add a new built-in plugin:
13
+ * 1. Import registerBuiltinPlugin from '../builtinPlugins.js'
14
+ * 2. Call registerBuiltinPlugin() with the plugin definition here
15
+ */
16
+
17
+ /**
18
+ * Initialize built-in plugins. Called during CLI startup.
19
+ */
20
+ export function initBuiltinPlugins(): void {
21
+ // No built-in plugins registered yet — this is the scaffolding for
22
+ // migrating bundled skills that should be user-toggleable.
23
+ }
Binary file
Binary file
@@ -0,0 +1,88 @@
1
+ /* eslint-disable eslint-plugin-n/no-unsupported-features/node-builtins */
2
+
3
+ import { errorMessage } from '../utils/errors.js'
4
+ import { jsonStringify } from '../utils/slowOperations.js'
5
+ import type { DirectConnectConfig } from './directConnectManager.js'
6
+ import { connectResponseSchema } from './types.js'
7
+
8
+ /**
9
+ * Errors thrown by createDirectConnectSession when the connection fails.
10
+ */
11
+ export class DirectConnectError extends Error {
12
+ constructor(message: string) {
13
+ super(message)
14
+ this.name = 'DirectConnectError'
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Create a session on a direct-connect server.
20
+ *
21
+ * Posts to `${serverUrl}/sessions`, validates the response, and returns
22
+ * a DirectConnectConfig ready for use by the REPL or headless runner.
23
+ *
24
+ * Throws DirectConnectError on network, HTTP, or response-parsing failures.
25
+ */
26
+ export async function createDirectConnectSession({
27
+ serverUrl,
28
+ authToken,
29
+ cwd,
30
+ dangerouslySkipPermissions,
31
+ }: {
32
+ serverUrl: string
33
+ authToken?: string
34
+ cwd: string
35
+ dangerouslySkipPermissions?: boolean
36
+ }): Promise<{
37
+ config: DirectConnectConfig
38
+ workDir?: string
39
+ }> {
40
+ const headers: Record<string, string> = {
41
+ 'content-type': 'application/json',
42
+ }
43
+ if (authToken) {
44
+ headers['authorization'] = `Bearer ${authToken}`
45
+ }
46
+
47
+ let resp: Response
48
+ try {
49
+ resp = await fetch(`${serverUrl}/sessions`, {
50
+ method: 'POST',
51
+ headers,
52
+ body: jsonStringify({
53
+ cwd,
54
+ ...(dangerouslySkipPermissions && {
55
+ dangerously_skip_permissions: true,
56
+ }),
57
+ }),
58
+ })
59
+ } catch (err) {
60
+ throw new DirectConnectError(
61
+ `Failed to connect to server at ${serverUrl}: ${errorMessage(err)}`,
62
+ )
63
+ }
64
+
65
+ if (!resp.ok) {
66
+ throw new DirectConnectError(
67
+ `Failed to create session: ${resp.status} ${resp.statusText}`,
68
+ )
69
+ }
70
+
71
+ const result = connectResponseSchema().safeParse(await resp.json())
72
+ if (!result.success) {
73
+ throw new DirectConnectError(
74
+ `Invalid session response: ${result.error.message}`,
75
+ )
76
+ }
77
+
78
+ const data = result.data
79
+ return {
80
+ config: {
81
+ serverUrl,
82
+ sessionId: data.session_id,
83
+ wsUrl: data.ws_url,
84
+ authToken,
85
+ },
86
+ workDir: data.work_dir,
87
+ }
88
+ }
@@ -0,0 +1,213 @@
1
+ /* eslint-disable eslint-plugin-n/no-unsupported-features/node-builtins */
2
+
3
+ import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
4
+ import type {
5
+ SDKControlPermissionRequest,
6
+ StdoutMessage,
7
+ } from '../entrypoints/sdk/controlTypes.js'
8
+ import type { RemotePermissionResponse } from '../remote/RemoteSessionManager.js'
9
+ import { logForDebugging } from '../utils/debug.js'
10
+ import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
11
+ import type { RemoteMessageContent } from '../utils/teleport/api.js'
12
+
13
+ export type DirectConnectConfig = {
14
+ serverUrl: string
15
+ sessionId: string
16
+ wsUrl: string
17
+ authToken?: string
18
+ }
19
+
20
+ export type DirectConnectCallbacks = {
21
+ onMessage: (message: SDKMessage) => void
22
+ onPermissionRequest: (
23
+ request: SDKControlPermissionRequest,
24
+ requestId: string,
25
+ ) => void
26
+ onConnected?: () => void
27
+ onDisconnected?: () => void
28
+ onError?: (error: Error) => void
29
+ }
30
+
31
+ function isStdoutMessage(value: unknown): value is StdoutMessage {
32
+ return (
33
+ typeof value === 'object' &&
34
+ value !== null &&
35
+ 'type' in value &&
36
+ typeof value.type === 'string'
37
+ )
38
+ }
39
+
40
+ export class DirectConnectSessionManager {
41
+ private ws: WebSocket | null = null
42
+ private config: DirectConnectConfig
43
+ private callbacks: DirectConnectCallbacks
44
+
45
+ constructor(config: DirectConnectConfig, callbacks: DirectConnectCallbacks) {
46
+ this.config = config
47
+ this.callbacks = callbacks
48
+ }
49
+
50
+ connect(): void {
51
+ const headers: Record<string, string> = {}
52
+ if (this.config.authToken) {
53
+ headers['authorization'] = `Bearer ${this.config.authToken}`
54
+ }
55
+ // Bun's WebSocket supports headers option but the DOM typings don't
56
+ this.ws = new WebSocket(this.config.wsUrl, {
57
+ headers,
58
+ } as unknown as string[])
59
+
60
+ this.ws.addEventListener('open', () => {
61
+ this.callbacks.onConnected?.()
62
+ })
63
+
64
+ this.ws.addEventListener('message', event => {
65
+ const data = typeof event.data === 'string' ? event.data : ''
66
+ const lines = data.split('\n').filter((l: string) => l.trim())
67
+
68
+ for (const line of lines) {
69
+ let raw: unknown
70
+ try {
71
+ raw = jsonParse(line)
72
+ } catch {
73
+ continue
74
+ }
75
+
76
+ if (!isStdoutMessage(raw)) {
77
+ continue
78
+ }
79
+ const parsed = raw
80
+
81
+ // Handle control requests (permission requests)
82
+ if (parsed.type === 'control_request') {
83
+ if (parsed.request.subtype === 'can_use_tool') {
84
+ this.callbacks.onPermissionRequest(
85
+ parsed.request,
86
+ parsed.request_id,
87
+ )
88
+ } else {
89
+ // Send an error response for unrecognized subtypes so the
90
+ // server doesn't hang waiting for a reply that never comes.
91
+ logForDebugging(
92
+ `[DirectConnect] Unsupported control request subtype: ${parsed.request.subtype}`,
93
+ )
94
+ this.sendErrorResponse(
95
+ parsed.request_id,
96
+ `Unsupported control request subtype: ${parsed.request.subtype}`,
97
+ )
98
+ }
99
+ continue
100
+ }
101
+
102
+ // Forward SDK messages (assistant, result, system, etc.)
103
+ if (
104
+ parsed.type !== 'control_response' &&
105
+ parsed.type !== 'keep_alive' &&
106
+ parsed.type !== 'control_cancel_request' &&
107
+ parsed.type !== 'streamlined_text' &&
108
+ parsed.type !== 'streamlined_tool_use_summary' &&
109
+ !(parsed.type === 'system' && parsed.subtype === 'post_turn_summary')
110
+ ) {
111
+ this.callbacks.onMessage(parsed)
112
+ }
113
+ }
114
+ })
115
+
116
+ this.ws.addEventListener('close', () => {
117
+ this.callbacks.onDisconnected?.()
118
+ })
119
+
120
+ this.ws.addEventListener('error', () => {
121
+ this.callbacks.onError?.(new Error('WebSocket connection error'))
122
+ })
123
+ }
124
+
125
+ sendMessage(content: RemoteMessageContent): boolean {
126
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
127
+ return false
128
+ }
129
+
130
+ // Must match SDKUserMessage format expected by `--input-format stream-json`
131
+ const message = jsonStringify({
132
+ type: 'user',
133
+ message: {
134
+ role: 'user',
135
+ content: content,
136
+ },
137
+ parent_tool_use_id: null,
138
+ session_id: '',
139
+ })
140
+ this.ws.send(message)
141
+ return true
142
+ }
143
+
144
+ respondToPermissionRequest(
145
+ requestId: string,
146
+ result: RemotePermissionResponse,
147
+ ): void {
148
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
149
+ return
150
+ }
151
+
152
+ // Must match SDKControlResponse format expected by StructuredIO
153
+ const response = jsonStringify({
154
+ type: 'control_response',
155
+ response: {
156
+ subtype: 'success',
157
+ request_id: requestId,
158
+ response: {
159
+ behavior: result.behavior,
160
+ ...(result.behavior === 'allow'
161
+ ? { updatedInput: result.updatedInput }
162
+ : { message: result.message }),
163
+ },
164
+ },
165
+ })
166
+ this.ws.send(response)
167
+ }
168
+
169
+ /**
170
+ * Send an interrupt signal to cancel the current request
171
+ */
172
+ sendInterrupt(): void {
173
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
174
+ return
175
+ }
176
+
177
+ // Must match SDKControlRequest format expected by StructuredIO
178
+ const request = jsonStringify({
179
+ type: 'control_request',
180
+ request_id: crypto.randomUUID(),
181
+ request: {
182
+ subtype: 'interrupt',
183
+ },
184
+ })
185
+ this.ws.send(request)
186
+ }
187
+
188
+ private sendErrorResponse(requestId: string, error: string): void {
189
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
190
+ return
191
+ }
192
+ const response = jsonStringify({
193
+ type: 'control_response',
194
+ response: {
195
+ subtype: 'error',
196
+ request_id: requestId,
197
+ error,
198
+ },
199
+ })
200
+ this.ws.send(response)
201
+ }
202
+
203
+ disconnect(): void {
204
+ if (this.ws) {
205
+ this.ws.close()
206
+ this.ws = null
207
+ }
208
+ }
209
+
210
+ isConnected(): boolean {
211
+ return this.ws?.readyState === WebSocket.OPEN
212
+ }
213
+ }
@@ -0,0 +1,57 @@
1
+ import type { ChildProcess } from 'child_process'
2
+ import { z } from 'zod/v4'
3
+ import { lazySchema } from '../utils/lazySchema.js'
4
+
5
+ export const connectResponseSchema = lazySchema(() =>
6
+ z.object({
7
+ session_id: z.string(),
8
+ ws_url: z.string(),
9
+ work_dir: z.string().optional(),
10
+ }),
11
+ )
12
+
13
+ export type ServerConfig = {
14
+ port: number
15
+ host: string
16
+ authToken: string
17
+ unix?: string
18
+ /** Idle timeout for detached sessions (ms). 0 = never expire. */
19
+ idleTimeoutMs?: number
20
+ /** Maximum number of concurrent sessions. */
21
+ maxSessions?: number
22
+ /** Default workspace directory for sessions that don't specify cwd. */
23
+ workspace?: string
24
+ }
25
+
26
+ export type SessionState =
27
+ | 'starting'
28
+ | 'running'
29
+ | 'detached'
30
+ | 'stopping'
31
+ | 'stopped'
32
+
33
+ export type SessionInfo = {
34
+ id: string
35
+ status: SessionState
36
+ createdAt: number
37
+ workDir: string
38
+ process: ChildProcess | null
39
+ sessionKey?: string
40
+ }
41
+
42
+ /**
43
+ * Stable session key → session metadata. Persisted to ~/.claude/server-sessions.json
44
+ * so sessions can be resumed across server restarts.
45
+ */
46
+ export type SessionIndexEntry = {
47
+ /** Server-assigned session ID (matches the subprocess's claude session). */
48
+ sessionId: string
49
+ /** The claude transcript session ID for --resume. Same as sessionId for direct sessions. */
50
+ transcriptSessionId: string
51
+ cwd: string
52
+ permissionMode?: string
53
+ createdAt: number
54
+ lastActiveAt: number
55
+ }
56
+
57
+ export type SessionIndex = Record<string, SessionIndexEntry>