kimaki 0.4.45 → 0.4.47
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/cli.js +27 -2
- package/dist/commands/abort.js +2 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +5 -3
- package/dist/commands/fork.js +5 -3
- package/dist/commands/merge-worktree.js +2 -2
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +4 -2
- package/dist/commands/session.js +4 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +4 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +20 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +5 -3
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +69 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +2 -2
- package/dist/session-handler.js +93 -15
- package/dist/tools.js +2 -2
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +1 -1
- package/src/cli.ts +28 -2
- package/src/commands/abort.ts +2 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +6 -3
- package/src/commands/fork.ts +6 -3
- package/src/commands/merge-worktree.ts +2 -2
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +2 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +5 -2
- package/src/commands/session.ts +5 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +5 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +23 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +6 -3
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +82 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +2 -2
- package/src/session-handler.ts +105 -15
- package/src/tools.ts +2 -2
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/src/session-handler.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
25
25
|
import { formatPart } from './message-formatting.js'
|
|
26
26
|
import { getOpencodeSystemMessage, type WorktreeInfo } from './system-message.js'
|
|
27
|
-
import { createLogger } from './logger.js'
|
|
27
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
28
28
|
import { isAbortError } from './utils.js'
|
|
29
29
|
import {
|
|
30
30
|
showAskUserQuestionDropdowns,
|
|
@@ -38,9 +38,9 @@ import {
|
|
|
38
38
|
} from './commands/permissions.js'
|
|
39
39
|
import * as errore from 'errore'
|
|
40
40
|
|
|
41
|
-
const sessionLogger = createLogger(
|
|
42
|
-
const voiceLogger = createLogger(
|
|
43
|
-
const discordLogger = createLogger(
|
|
41
|
+
const sessionLogger = createLogger(LogPrefix.SESSION)
|
|
42
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
43
|
+
const discordLogger = createLogger(LogPrefix.DISCORD)
|
|
44
44
|
|
|
45
45
|
export const abortControllers = new Map<string, AbortController>()
|
|
46
46
|
|
|
@@ -86,6 +86,8 @@ export type QueuedMessage = {
|
|
|
86
86
|
// Key is threadId, value is array of queued messages
|
|
87
87
|
export const messageQueue = new Map<string, QueuedMessage[]>()
|
|
88
88
|
|
|
89
|
+
const activeEventHandlers = new Map<string, Promise<void>>()
|
|
90
|
+
|
|
89
91
|
export function addToQueue({
|
|
90
92
|
threadId,
|
|
91
93
|
message,
|
|
@@ -132,7 +134,7 @@ export async function abortAndRetrySession({
|
|
|
132
134
|
sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`)
|
|
133
135
|
|
|
134
136
|
// Abort with special reason so we don't show "completed" message
|
|
135
|
-
controller.abort('model-change')
|
|
137
|
+
controller.abort(new Error('model-change'))
|
|
136
138
|
|
|
137
139
|
// Also call the API abort endpoint
|
|
138
140
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
@@ -301,6 +303,15 @@ export async function handleOpencodeSession({
|
|
|
301
303
|
if (existingController) {
|
|
302
304
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`)
|
|
303
305
|
existingController.abort(new Error('New request started'))
|
|
306
|
+
const abortResult = await errore.tryAsync(() => {
|
|
307
|
+
return getClient().session.abort({
|
|
308
|
+
path: { id: session.id },
|
|
309
|
+
query: { directory: sdkDirectory },
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
if (abortResult instanceof Error) {
|
|
313
|
+
sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult)
|
|
314
|
+
}
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
// Auto-reject ALL pending permissions for this thread
|
|
@@ -339,10 +350,10 @@ export async function handleOpencodeSession({
|
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
352
|
|
|
342
|
-
//
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
345
|
-
sessionLogger.log(`[QUESTION]
|
|
353
|
+
// Answer any pending question tool with the user's message (silently, no thread message)
|
|
354
|
+
const questionAnswered = await cancelPendingQuestion(thread.id, prompt)
|
|
355
|
+
if (questionAnswered) {
|
|
356
|
+
sessionLogger.log(`[QUESTION] Answered pending question with user message`)
|
|
346
357
|
}
|
|
347
358
|
|
|
348
359
|
const abortController = new AbortController()
|
|
@@ -363,6 +374,17 @@ export async function handleOpencodeSession({
|
|
|
363
374
|
return
|
|
364
375
|
}
|
|
365
376
|
|
|
377
|
+
const previousHandler = activeEventHandlers.get(thread.id)
|
|
378
|
+
if (previousHandler) {
|
|
379
|
+
sessionLogger.log(`[EVENT] Waiting for previous handler to finish`)
|
|
380
|
+
await Promise.race([
|
|
381
|
+
previousHandler,
|
|
382
|
+
new Promise((resolve) => {
|
|
383
|
+
setTimeout(resolve, 1000)
|
|
384
|
+
}),
|
|
385
|
+
])
|
|
386
|
+
}
|
|
387
|
+
|
|
366
388
|
// Use v2 client for event subscription (has proper types for question.asked events)
|
|
367
389
|
const clientV2 = getOpencodeClientV2(directory)
|
|
368
390
|
if (!clientV2) {
|
|
@@ -396,8 +418,10 @@ export async function handleOpencodeSession({
|
|
|
396
418
|
let usedAgent: string | undefined
|
|
397
419
|
let tokensUsedInSession = 0
|
|
398
420
|
let lastDisplayedContextPercentage = 0
|
|
421
|
+
let lastRateLimitDisplayTime = 0
|
|
399
422
|
let modelContextLimit: number | undefined
|
|
400
423
|
let assistantMessageId: string | undefined
|
|
424
|
+
let handlerPromise: Promise<void> | null = null
|
|
401
425
|
|
|
402
426
|
let typingInterval: NodeJS.Timeout | null = null
|
|
403
427
|
|
|
@@ -726,6 +750,10 @@ export async function handleOpencodeSession({
|
|
|
726
750
|
part: Part,
|
|
727
751
|
subtaskInfo: { label: string; assistantMessageId?: string },
|
|
728
752
|
) => {
|
|
753
|
+
// In text-only mode, skip all subtask output (they're tool-related)
|
|
754
|
+
if (verbosity === 'text-only') {
|
|
755
|
+
return
|
|
756
|
+
}
|
|
729
757
|
if (part.type === 'step-start' || part.type === 'step-finish') {
|
|
730
758
|
return
|
|
731
759
|
}
|
|
@@ -984,10 +1012,53 @@ export async function handleOpencodeSession({
|
|
|
984
1012
|
})
|
|
985
1013
|
}
|
|
986
1014
|
|
|
1015
|
+
const handleSessionStatus = async (properties: {
|
|
1016
|
+
sessionID: string
|
|
1017
|
+
status: { type: 'idle' } | { type: 'retry'; attempt: number; message: string; next: number } | { type: 'busy' }
|
|
1018
|
+
}) => {
|
|
1019
|
+
if (properties.sessionID !== session.id) {
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
1022
|
+
if (properties.status.type !== 'retry') {
|
|
1023
|
+
return
|
|
1024
|
+
}
|
|
1025
|
+
// Throttle to once per 10 seconds
|
|
1026
|
+
const now = Date.now()
|
|
1027
|
+
if (now - lastRateLimitDisplayTime < 10_000) {
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
lastRateLimitDisplayTime = now
|
|
1031
|
+
|
|
1032
|
+
const { attempt, message, next } = properties.status
|
|
1033
|
+
const remainingMs = Math.max(0, next - now)
|
|
1034
|
+
const remainingSec = Math.ceil(remainingMs / 1000)
|
|
1035
|
+
|
|
1036
|
+
const duration = (() => {
|
|
1037
|
+
if (remainingSec < 60) {
|
|
1038
|
+
return `${remainingSec}s`
|
|
1039
|
+
}
|
|
1040
|
+
const mins = Math.floor(remainingSec / 60)
|
|
1041
|
+
const secs = remainingSec % 60
|
|
1042
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
1043
|
+
})()
|
|
1044
|
+
|
|
1045
|
+
const chunk = `⬦ ${message} - retrying in ${duration} (attempt #${attempt})`
|
|
1046
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
1047
|
+
}
|
|
1048
|
+
|
|
987
1049
|
const handleSessionIdle = (idleSessionId: string) => {
|
|
988
1050
|
if (idleSessionId === session.id) {
|
|
1051
|
+
// Ignore stale session.idle events - if we haven't received any content yet
|
|
1052
|
+
// (no assistantMessageId set), this is likely a stale event from before
|
|
1053
|
+
// the prompt was sent or from a previous request's subscription state.
|
|
1054
|
+
if (!assistantMessageId) {
|
|
1055
|
+
sessionLogger.log(
|
|
1056
|
+
`[SESSION IDLE] Ignoring stale idle event for ${session.id} (no content received yet)`,
|
|
1057
|
+
)
|
|
1058
|
+
return
|
|
1059
|
+
}
|
|
989
1060
|
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
|
|
990
|
-
abortController.abort('finished')
|
|
1061
|
+
abortController.abort(new Error('finished'))
|
|
991
1062
|
return
|
|
992
1063
|
}
|
|
993
1064
|
|
|
@@ -1024,6 +1095,9 @@ export async function handleOpencodeSession({
|
|
|
1024
1095
|
case 'session.idle':
|
|
1025
1096
|
handleSessionIdle(event.properties.sessionID)
|
|
1026
1097
|
break
|
|
1098
|
+
case 'session.status':
|
|
1099
|
+
await handleSessionStatus(event.properties)
|
|
1100
|
+
break
|
|
1027
1101
|
default:
|
|
1028
1102
|
break
|
|
1029
1103
|
}
|
|
@@ -1052,7 +1126,8 @@ export async function handleOpencodeSession({
|
|
|
1052
1126
|
stopTyping = null
|
|
1053
1127
|
}
|
|
1054
1128
|
|
|
1055
|
-
|
|
1129
|
+
const abortReason = (abortController.signal.reason as Error)?.message
|
|
1130
|
+
if (!abortController.signal.aborted || abortReason === 'finished') {
|
|
1056
1131
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime)
|
|
1057
1132
|
const attachCommand = port ? ` ⋅ ${session.id}` : ''
|
|
1058
1133
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : ''
|
|
@@ -1143,7 +1218,7 @@ export async function handleOpencodeSession({
|
|
|
1143
1218
|
}
|
|
1144
1219
|
} else {
|
|
1145
1220
|
sessionLogger.log(
|
|
1146
|
-
`Session was aborted (reason: ${
|
|
1221
|
+
`Session was aborted (reason: ${abortReason}), skipping duration message`,
|
|
1147
1222
|
)
|
|
1148
1223
|
}
|
|
1149
1224
|
}
|
|
@@ -1151,7 +1226,13 @@ export async function handleOpencodeSession({
|
|
|
1151
1226
|
|
|
1152
1227
|
const promptResult: Error | { sessionID: string; result: any; port?: number } | undefined =
|
|
1153
1228
|
await errore.tryAsync(async () => {
|
|
1154
|
-
const
|
|
1229
|
+
const newHandlerPromise = eventHandler().finally(() => {
|
|
1230
|
+
if (activeEventHandlers.get(thread.id) === newHandlerPromise) {
|
|
1231
|
+
activeEventHandlers.delete(thread.id)
|
|
1232
|
+
}
|
|
1233
|
+
})
|
|
1234
|
+
activeEventHandlers.set(thread.id, newHandlerPromise)
|
|
1235
|
+
handlerPromise = newHandlerPromise
|
|
1155
1236
|
|
|
1156
1237
|
if (abortController.signal.aborted) {
|
|
1157
1238
|
sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`)
|
|
@@ -1256,7 +1337,7 @@ export async function handleOpencodeSession({
|
|
|
1256
1337
|
throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`)
|
|
1257
1338
|
}
|
|
1258
1339
|
|
|
1259
|
-
abortController.abort('finished')
|
|
1340
|
+
abortController.abort(new Error('finished'))
|
|
1260
1341
|
|
|
1261
1342
|
sessionLogger.log(`Successfully sent prompt, got response`)
|
|
1262
1343
|
|
|
@@ -1273,6 +1354,15 @@ export async function handleOpencodeSession({
|
|
|
1273
1354
|
return { sessionID: session.id, result: response.data, port }
|
|
1274
1355
|
})
|
|
1275
1356
|
|
|
1357
|
+
if (handlerPromise) {
|
|
1358
|
+
await Promise.race([
|
|
1359
|
+
handlerPromise,
|
|
1360
|
+
new Promise((resolve) => {
|
|
1361
|
+
setTimeout(resolve, 1000)
|
|
1362
|
+
}),
|
|
1363
|
+
])
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1276
1366
|
if (!errore.isError(promptResult)) {
|
|
1277
1367
|
return promptResult
|
|
1278
1368
|
}
|
|
@@ -1283,7 +1373,7 @@ export async function handleOpencodeSession({
|
|
|
1283
1373
|
}
|
|
1284
1374
|
|
|
1285
1375
|
sessionLogger.error(`ERROR: Failed to send prompt:`, promptError)
|
|
1286
|
-
abortController.abort('error')
|
|
1376
|
+
abortController.abort(new Error('error'))
|
|
1287
1377
|
|
|
1288
1378
|
if (originalMessage) {
|
|
1289
1379
|
const reactionResult = await errore.tryAsync(async () => {
|
package/src/tools.ts
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
type AssistantMessage,
|
|
13
13
|
type Provider,
|
|
14
14
|
} from '@opencode-ai/sdk'
|
|
15
|
-
import { createLogger } from './logger.js'
|
|
15
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
16
16
|
import * as errore from 'errore'
|
|
17
17
|
|
|
18
|
-
const toolsLogger = createLogger(
|
|
18
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS)
|
|
19
19
|
|
|
20
20
|
import { ShareMarkdown } from './markdown.js'
|
|
21
21
|
import { formatDistanceToNow } from './utils.js'
|
package/src/voice-handler.ts
CHANGED
|
@@ -37,9 +37,9 @@ import {
|
|
|
37
37
|
import { transcribeAudio } from './voice.js'
|
|
38
38
|
import { FetchError } from './errors.js'
|
|
39
39
|
|
|
40
|
-
import { createLogger } from './logger.js'
|
|
40
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
41
41
|
|
|
42
|
-
const voiceLogger = createLogger(
|
|
42
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
43
43
|
|
|
44
44
|
export type VoiceConnectionData = {
|
|
45
45
|
connection: VoiceConnection
|
package/src/voice.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { GoogleGenAI, Type, type Content, type Part, type Tool } from '@google/genai'
|
|
7
7
|
import * as errore from 'errore'
|
|
8
|
-
import { createLogger } from './logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
9
|
import { glob } from 'glob'
|
|
10
10
|
import { ripGrep } from 'ripgrep-js'
|
|
11
11
|
import {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
GlobSearchError,
|
|
20
20
|
} from './errors.js'
|
|
21
21
|
|
|
22
|
-
const voiceLogger = createLogger(
|
|
22
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
23
23
|
|
|
24
24
|
export type TranscriptionToolRunner = ({
|
|
25
25
|
name,
|
package/src/worktree-utils.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Worktree utility functions.
|
|
2
2
|
// Wrapper for OpenCode worktree creation that also initializes git submodules.
|
|
3
|
+
// Also handles capturing and applying git diffs when creating worktrees from threads.
|
|
3
4
|
|
|
4
|
-
import { exec } from 'node:child_process'
|
|
5
|
+
import { exec, spawn } from 'node:child_process'
|
|
5
6
|
import { promisify } from 'node:util'
|
|
6
|
-
import { createLogger } from './logger.js'
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
7
8
|
import type { getOpencodeClientV2 } from './opencode.js'
|
|
8
9
|
|
|
9
10
|
export const execAsync = promisify(exec)
|
|
10
11
|
|
|
11
|
-
const logger = createLogger(
|
|
12
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
12
13
|
|
|
13
14
|
type OpencodeClientV2 = NonNullable<ReturnType<typeof getOpencodeClientV2>>
|
|
14
15
|
|
|
@@ -20,16 +21,21 @@ type WorktreeResult = {
|
|
|
20
21
|
/**
|
|
21
22
|
* Create a worktree using OpenCode SDK and initialize git submodules.
|
|
22
23
|
* This wrapper ensures submodules are properly set up in new worktrees.
|
|
24
|
+
*
|
|
25
|
+
* If diff is provided, it's applied BEFORE submodule update to ensure
|
|
26
|
+
* any submodule pointer changes in the diff are respected.
|
|
23
27
|
*/
|
|
24
28
|
export async function createWorktreeWithSubmodules({
|
|
25
29
|
clientV2,
|
|
26
30
|
directory,
|
|
27
31
|
name,
|
|
32
|
+
diff,
|
|
28
33
|
}: {
|
|
29
34
|
clientV2: OpencodeClientV2
|
|
30
35
|
directory: string
|
|
31
36
|
name: string
|
|
32
|
-
|
|
37
|
+
diff?: CapturedDiff | null
|
|
38
|
+
}): Promise<WorktreeResult & { diffApplied: boolean } | Error> {
|
|
33
39
|
// 1. Create worktree via OpenCode SDK
|
|
34
40
|
const response = await clientV2.worktree.create({
|
|
35
41
|
directory,
|
|
@@ -45,8 +51,19 @@ export async function createWorktreeWithSubmodules({
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const worktreeDir = response.data.directory
|
|
54
|
+
let diffApplied = false
|
|
48
55
|
|
|
49
|
-
// 2.
|
|
56
|
+
// 2. Apply diff BEFORE submodule update (if provided)
|
|
57
|
+
// This ensures any submodule pointer changes in the diff are applied first,
|
|
58
|
+
// so submodule update checks out the correct commits.
|
|
59
|
+
if (diff) {
|
|
60
|
+
logger.log(`Applying diff to ${worktreeDir} before submodule init`)
|
|
61
|
+
diffApplied = await applyGitDiff(worktreeDir, diff)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Init submodules in new worktree (don't block on failure)
|
|
65
|
+
// Uses --init to initialize, --recursive for nested submodules.
|
|
66
|
+
// Submodules will be checked out at the commit specified by the (possibly updated) index.
|
|
50
67
|
try {
|
|
51
68
|
logger.log(`Initializing submodules in ${worktreeDir}`)
|
|
52
69
|
await execAsync('git submodule update --init --recursive', {
|
|
@@ -60,7 +77,7 @@ export async function createWorktreeWithSubmodules({
|
|
|
60
77
|
)
|
|
61
78
|
}
|
|
62
79
|
|
|
63
|
-
//
|
|
80
|
+
// 4. Install dependencies using ni (detects package manager from lockfile)
|
|
64
81
|
try {
|
|
65
82
|
logger.log(`Installing dependencies in ${worktreeDir}`)
|
|
66
83
|
await execAsync('npx -y ni', {
|
|
@@ -74,5 +91,92 @@ export async function createWorktreeWithSubmodules({
|
|
|
74
91
|
)
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
return response.data
|
|
94
|
+
return { ...response.data, diffApplied }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Captured git diff (both staged and unstaged changes).
|
|
99
|
+
*/
|
|
100
|
+
export type CapturedDiff = {
|
|
101
|
+
unstaged: string
|
|
102
|
+
staged: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Capture git diff from a directory (both staged and unstaged changes).
|
|
107
|
+
* Returns null if no changes or on error.
|
|
108
|
+
*/
|
|
109
|
+
export async function captureGitDiff(directory: string): Promise<CapturedDiff | null> {
|
|
110
|
+
try {
|
|
111
|
+
// Capture unstaged changes
|
|
112
|
+
const unstagedResult = await execAsync('git diff', { cwd: directory })
|
|
113
|
+
const unstaged = unstagedResult.stdout.trim()
|
|
114
|
+
|
|
115
|
+
// Capture staged changes
|
|
116
|
+
const stagedResult = await execAsync('git diff --staged', { cwd: directory })
|
|
117
|
+
const staged = stagedResult.stdout.trim()
|
|
118
|
+
|
|
119
|
+
if (!unstaged && !staged) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { unstaged, staged }
|
|
124
|
+
} catch (e) {
|
|
125
|
+
logger.warn(`Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`)
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run a git command with stdin input.
|
|
132
|
+
* Uses spawn to pipe the diff content to git apply.
|
|
133
|
+
*/
|
|
134
|
+
function runGitWithStdin(args: string[], cwd: string, input: string): Promise<void> {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
137
|
+
|
|
138
|
+
let stderr = ''
|
|
139
|
+
child.stderr?.on('data', (data) => {
|
|
140
|
+
stderr += data.toString()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
child.on('close', (code) => {
|
|
144
|
+
if (code === 0) {
|
|
145
|
+
resolve()
|
|
146
|
+
} else {
|
|
147
|
+
reject(new Error(stderr || `git ${args.join(' ')} failed with code ${code}`))
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
child.on('error', reject)
|
|
152
|
+
|
|
153
|
+
child.stdin?.write(input)
|
|
154
|
+
child.stdin?.end()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Apply a captured git diff to a directory.
|
|
160
|
+
* Applies staged changes first, then unstaged.
|
|
161
|
+
*/
|
|
162
|
+
export async function applyGitDiff(directory: string, diff: CapturedDiff): Promise<boolean> {
|
|
163
|
+
try {
|
|
164
|
+
// Apply staged changes first (and stage them)
|
|
165
|
+
if (diff.staged) {
|
|
166
|
+
logger.log(`Applying staged diff to ${directory}`)
|
|
167
|
+
await runGitWithStdin(['apply', '--index'], directory, diff.staged)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Apply unstaged changes (don't stage them)
|
|
171
|
+
if (diff.unstaged) {
|
|
172
|
+
logger.log(`Applying unstaged diff to ${directory}`)
|
|
173
|
+
await runGitWithStdin(['apply'], directory, diff.unstaged)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger.log(`Successfully applied diff to ${directory}`)
|
|
177
|
+
return true
|
|
178
|
+
} catch (e) {
|
|
179
|
+
logger.warn(`Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`)
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
78
182
|
}
|
package/src/xml.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { DomHandler, Parser, ElementType } from 'htmlparser2'
|
|
6
6
|
import type { ChildNode, Element, Text } from 'domhandler'
|
|
7
|
-
import { createLogger } from './logger.js'
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
8
8
|
|
|
9
|
-
const xmlLogger = createLogger(
|
|
9
|
+
const xmlLogger = createLogger(LogPrefix.XML)
|
|
10
10
|
|
|
11
11
|
export function extractTagsArrays<T extends string>({
|
|
12
12
|
xml,
|