kimaki 0.4.77 → 0.4.78

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,107 @@
1
+ // Shared apply_patch text parsing utilities.
2
+ // Used by diff-patch-plugin.ts (file path extraction for snapshots) and
3
+ // message-formatting.ts (per-file addition/deletion counts for Discord display).
4
+ //
5
+ // The apply_patch tool uses three path header formats:
6
+ // *** Add File: path — new file
7
+ // *** Update File: path — existing file edit
8
+ // *** Delete File: path — file removal
9
+ // *** Move to: path — rename destination
10
+ // --- a/path / +++ b/path — unified diff headers (fallback)
11
+
12
+ /**
13
+ * Extract all file paths referenced in a patchText string.
14
+ * Handles custom apply_patch headers, move targets, and unified diff headers.
15
+ * Returns deduplicated paths.
16
+ */
17
+ export function extractPatchFilePaths(patchText: string): string[] {
18
+ const custom = [
19
+ ...patchText.matchAll(
20
+ /^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm,
21
+ ),
22
+ ].map((m) => {
23
+ return (m[1] ?? '').trim()
24
+ })
25
+ const moved = [
26
+ ...patchText.matchAll(/^\*\*\* Move to:\s+(.+)$/gm),
27
+ ].map((m) => {
28
+ return (m[1] ?? '').trim()
29
+ })
30
+ const unified = [
31
+ ...patchText.matchAll(/^(?:---|\+\+\+) [ab]\/(.+)$/gm),
32
+ ].map((m) => {
33
+ return (m[1] ?? '').trim()
34
+ })
35
+ const all = [...custom, ...moved, ...unified].filter(Boolean)
36
+ return all.filter((v, i, a) => {
37
+ return a.indexOf(v) === i
38
+ })
39
+ }
40
+
41
+ /**
42
+ * Parse a patchText string and count additions/deletions per file.
43
+ * Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
44
+ * with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
45
+ */
46
+ export function parsePatchFileCounts(
47
+ patchText: string,
48
+ ): Map<string, { additions: number; deletions: number }> {
49
+ const counts = new Map<string, { additions: number; deletions: number }>()
50
+ const lines = patchText.split('\n')
51
+ let currentFile = ''
52
+ let currentType = ''
53
+ let inHunk = false
54
+
55
+ for (const line of lines) {
56
+ const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
57
+ const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
58
+ const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
59
+
60
+ if (addMatch || updateMatch || deleteMatch) {
61
+ const match = addMatch || updateMatch || deleteMatch
62
+ currentFile = (match?.[1] ?? '').trim()
63
+ currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
64
+ counts.set(currentFile, { additions: 0, deletions: 0 })
65
+ inHunk = false
66
+ continue
67
+ }
68
+
69
+ if (line.startsWith('@@')) {
70
+ inHunk = true
71
+ continue
72
+ }
73
+
74
+ if (line.startsWith('*** ')) {
75
+ inHunk = false
76
+ continue
77
+ }
78
+
79
+ if (!currentFile) {
80
+ continue
81
+ }
82
+
83
+ const entry = counts.get(currentFile)
84
+ if (!entry) {
85
+ continue
86
+ }
87
+
88
+ if (currentType === 'add') {
89
+ // all content lines in Add File are additions
90
+ if (line.length > 0 && !line.startsWith('*** ')) {
91
+ entry.additions++
92
+ }
93
+ } else if (currentType === 'delete') {
94
+ // all content lines in Delete File are deletions
95
+ if (line.length > 0 && !line.startsWith('*** ')) {
96
+ entry.deletions++
97
+ }
98
+ } else if (inHunk) {
99
+ if (line.startsWith('+')) {
100
+ entry.additions++
101
+ } else if (line.startsWith('-')) {
102
+ entry.deletions++
103
+ }
104
+ }
105
+ }
106
+ return counts
107
+ }
@@ -123,6 +123,7 @@ import {
123
123
  matchThinkingValue,
124
124
  } from '../thinking-utils.js'
125
125
  import { execAsync } from '../worktrees.js'
126
+
126
127
  import { notifyError } from '../sentry.js'
127
128
  import { createDebouncedProcessFlush } from '../debounced-process-flush.js'
128
129
  import { cancelHtmlActionsForThread } from '../html-actions.js'
@@ -3534,7 +3535,7 @@ export class ThreadSessionRuntime {
3534
3535
 
3535
3536
  const client = getOpencodeClient(this.projectDirectory)
3536
3537
 
3537
- // Run git branch, token fetch, and provider list in parallel
3538
+ // Run git branch and token fetch in parallel (fast, no external CLI)
3538
3539
  const [branchResult, contextResult] = await Promise.all([
3539
3540
  errore.tryAsync(() => {
3540
3541
  return execAsync('git symbolic-ref --short HEAD', {
@@ -0,0 +1,101 @@
1
+ // In-process WebSocket-to-TCP bridge (websockify replacement).
2
+ // Accepts WebSocket connections and pipes raw bytes to/from a TCP target.
3
+ // Used by /screenshare to bridge noVNC (WebSocket) to a VNC server (TCP).
4
+ // Supports the 'binary' subprotocol required by noVNC.
5
+
6
+ import { WebSocketServer, WebSocket } from 'ws'
7
+ import net from 'node:net'
8
+ import { createLogger } from './logger.js'
9
+
10
+ const logger = createLogger('SCREEN')
11
+
12
+ type WebsockifyOptions = {
13
+ /** Port for the WebSocket server (0 = auto-assign) */
14
+ wsPort: number
15
+ /** TCP target host */
16
+ tcpHost: string
17
+ /** TCP target port */
18
+ tcpPort: number
19
+ }
20
+
21
+ type WebsockifyInstance = {
22
+ wss: WebSocketServer
23
+ /** Resolved port (useful when wsPort=0) */
24
+ port: number
25
+ close: () => void
26
+ }
27
+
28
+ export function startWebsockify({
29
+ wsPort,
30
+ tcpHost,
31
+ tcpPort,
32
+ }: WebsockifyOptions): Promise<WebsockifyInstance> {
33
+ return new Promise((resolve, reject) => {
34
+ const wss = new WebSocketServer({
35
+ port: wsPort,
36
+ // noVNC negotiates the 'binary' subprotocol
37
+ handleProtocols: (protocols) => {
38
+ if (protocols.has('binary')) {
39
+ return 'binary'
40
+ }
41
+ return false
42
+ },
43
+ })
44
+
45
+ wss.on('listening', () => {
46
+ const addr = wss.address()
47
+ const port = typeof addr === 'object' && addr ? addr.port : wsPort
48
+ logger.log(`Websockify listening on port ${port} → ${tcpHost}:${tcpPort}`)
49
+ resolve({
50
+ wss,
51
+ port,
52
+ close: () => {
53
+ for (const client of wss.clients) {
54
+ client.close()
55
+ }
56
+ wss.close()
57
+ },
58
+ })
59
+ })
60
+
61
+ wss.on('error', (err) => {
62
+ reject(new Error('Websockify failed to start', { cause: err }))
63
+ })
64
+
65
+ wss.on('connection', (ws) => {
66
+ const tcp = net.createConnection(tcpPort, tcpHost, () => {
67
+ logger.log(`TCP connection established to ${tcpHost}:${tcpPort}`)
68
+ })
69
+
70
+ tcp.on('data', (data) => {
71
+ if (ws.readyState === WebSocket.OPEN) {
72
+ ws.send(data)
73
+ }
74
+ })
75
+
76
+ ws.on('message', (data: Buffer) => {
77
+ if (!tcp.destroyed) {
78
+ tcp.write(data)
79
+ }
80
+ })
81
+
82
+ ws.on('close', () => {
83
+ tcp.destroy()
84
+ })
85
+
86
+ ws.on('error', (err) => {
87
+ logger.error('WebSocket error:', err)
88
+ tcp.destroy()
89
+ })
90
+
91
+ tcp.on('close', () => {
92
+ ws.close()
93
+ })
94
+
95
+ tcp.on('error', (err) => {
96
+ logger.error('TCP connection error:', err)
97
+ ws.close()
98
+ })
99
+ })
100
+ })
101
+ }