@co0ontty/wand 1.6.2 → 1.9.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/config.js +40 -0
- package/dist/git-worktree.d.ts +28 -0
- package/dist/git-worktree.js +287 -0
- package/dist/message-parser.d.ts +1 -1
- package/dist/message-parser.js +275 -1
- package/dist/process-manager.d.ts +6 -3
- package/dist/process-manager.js +135 -81
- package/dist/pty-text-utils.js +79 -29
- package/dist/server-session-routes.js +192 -7
- package/dist/server.js +38 -1
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +23 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +258 -58
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +107 -43
- package/dist/types.d.ts +65 -0
- package/dist/web-ui/content/scripts.js +2251 -500
- package/dist/web-ui/content/styles.css +1154 -119
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { WandStorage } from "./storage.js";
|
|
3
|
-
import { ExecutionMode, ProcessEventHandler, SessionSnapshot, WandConfig } from "./types.js";
|
|
3
|
+
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, WandConfig } from "./types.js";
|
|
4
4
|
export type { ProcessEvent, ProcessEventHandler } from "./types.js";
|
|
5
5
|
/** Human-readable task information for the UI */
|
|
6
6
|
export interface TaskInfo {
|
|
@@ -32,8 +32,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
32
32
|
private readonly lifecycleManager;
|
|
33
33
|
/** Per-session debounce timers for throttled persist calls */
|
|
34
34
|
private readonly persistDebounceTimers;
|
|
35
|
-
/** Last persisted message
|
|
36
|
-
private readonly
|
|
35
|
+
/** Last persisted message state per session — used to skip redundant message writes */
|
|
36
|
+
private readonly lastPersistedMessageState;
|
|
37
37
|
constructor(config: WandConfig, storage: WandStorage, configDir?: string);
|
|
38
38
|
on(event: "process", listener: ProcessEventHandler): this;
|
|
39
39
|
private emitEvent;
|
|
@@ -41,6 +41,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
41
41
|
start(command: string, cwd: string | undefined, mode: ExecutionMode, initialInput?: string, opts?: {
|
|
42
42
|
resumedFromSessionId?: string;
|
|
43
43
|
autoRecovered?: boolean;
|
|
44
|
+
worktreeEnabled?: boolean;
|
|
45
|
+
provider?: SessionProvider;
|
|
44
46
|
}): SessionSnapshot;
|
|
45
47
|
list(): SessionSnapshot[];
|
|
46
48
|
hasClaudeSessionFile(cwd: string, claudeSessionId: string): boolean;
|
|
@@ -52,6 +54,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
52
54
|
cwd: string;
|
|
53
55
|
}[]): number;
|
|
54
56
|
get(id: string): SessionSnapshot | null;
|
|
57
|
+
getPtyTranscript(id: string): string | null;
|
|
55
58
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
56
59
|
/** Emit a task event for a session, debounced to avoid flooding */
|
|
57
60
|
private emitTask;
|
package/dist/process-manager.js
CHANGED
|
@@ -9,10 +9,13 @@ import { SessionLogger } from "./session-logger.js";
|
|
|
9
9
|
import { SessionLifecycleManager } from "./session-lifecycle.js";
|
|
10
10
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
11
11
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
|
|
12
|
+
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
12
13
|
import { getResumeCommandSessionId, hasRealConversationMessages, } from "./resume-policy.js";
|
|
13
|
-
|
|
14
|
+
function resolveProviderFromCommand(command) {
|
|
15
|
+
return /^codex\b/.test(command.trim()) ? "codex" : "claude";
|
|
16
|
+
}
|
|
14
17
|
function isRunningAsRoot() {
|
|
15
|
-
return process.getuid
|
|
18
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
16
19
|
}
|
|
17
20
|
export class SessionInputError extends Error {
|
|
18
21
|
code;
|
|
@@ -398,6 +401,32 @@ function getLatestClaudeTaskId(excludeIds) {
|
|
|
398
401
|
}
|
|
399
402
|
}
|
|
400
403
|
/** Derive a short summary for a session from user messages or current task. */
|
|
404
|
+
function getLastMessageSignature(messages) {
|
|
405
|
+
const last = messages[messages.length - 1];
|
|
406
|
+
if (!last) {
|
|
407
|
+
return "";
|
|
408
|
+
}
|
|
409
|
+
const lastContent = JSON.stringify(last.content ?? []);
|
|
410
|
+
return `${last.role}:${lastContent}`;
|
|
411
|
+
}
|
|
412
|
+
function getPersistedMessageState(messages) {
|
|
413
|
+
return {
|
|
414
|
+
count: messages.length,
|
|
415
|
+
signature: getLastMessageSignature(messages),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function shouldPersistMessages(lastState, nextMessages) {
|
|
419
|
+
if (nextMessages.length === 0) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const nextState = getPersistedMessageState(nextMessages);
|
|
423
|
+
return !lastState
|
|
424
|
+
|| lastState.count !== nextState.count
|
|
425
|
+
|| lastState.signature !== nextState.signature;
|
|
426
|
+
}
|
|
427
|
+
function recoverMessagesFromSnapshot(snapshot) {
|
|
428
|
+
return snapshot.messages ?? [];
|
|
429
|
+
}
|
|
401
430
|
function deriveSessionSummary(messages, currentTaskTitle) {
|
|
402
431
|
// Prefer first user message as summary
|
|
403
432
|
for (const msg of messages) {
|
|
@@ -408,9 +437,8 @@ function deriveSessionSummary(messages, currentTaskTitle) {
|
|
|
408
437
|
return block.text.trim().slice(0, 120);
|
|
409
438
|
}
|
|
410
439
|
}
|
|
411
|
-
break;
|
|
440
|
+
break;
|
|
412
441
|
}
|
|
413
|
-
// Fallback to current task title
|
|
414
442
|
if (currentTaskTitle)
|
|
415
443
|
return currentTaskTitle.slice(0, 120);
|
|
416
444
|
return undefined;
|
|
@@ -423,8 +451,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
423
451
|
lifecycleManager;
|
|
424
452
|
/** Per-session debounce timers for throttled persist calls */
|
|
425
453
|
persistDebounceTimers = new Map();
|
|
426
|
-
/** Last persisted message
|
|
427
|
-
|
|
454
|
+
/** Last persisted message state per session — used to skip redundant message writes */
|
|
455
|
+
lastPersistedMessageState = new Map();
|
|
428
456
|
constructor(config, storage, configDir) {
|
|
429
457
|
super();
|
|
430
458
|
this.config = config;
|
|
@@ -444,23 +472,31 @@ export class ProcessManager extends EventEmitter {
|
|
|
444
472
|
if ((snapshot.sessionKind ?? "pty") !== "pty") {
|
|
445
473
|
continue;
|
|
446
474
|
}
|
|
447
|
-
const
|
|
448
|
-
const
|
|
475
|
+
const provider = snapshot.provider ?? resolveProviderFromCommand(snapshot.command);
|
|
476
|
+
const isClaudeCmd = provider === "claude";
|
|
477
|
+
const resumeCommandSessionId = isClaudeCmd ? getResumeCommandSessionId(snapshot.command) : null;
|
|
449
478
|
// Sessions restored from storage have ptyProcess: null — the old server's PTY
|
|
450
479
|
// belongs to a dead process. Mark running sessions as exited so the UI
|
|
451
480
|
// reflects reality and users can start fresh sessions.
|
|
452
481
|
if (snapshot.status === "running") {
|
|
453
|
-
const
|
|
482
|
+
const recoveredMessages = recoverMessagesFromSnapshot(snapshot);
|
|
483
|
+
const updated = {
|
|
484
|
+
...snapshot,
|
|
485
|
+
status: "exited",
|
|
486
|
+
endedAt: new Date().toISOString(),
|
|
487
|
+
messages: recoveredMessages.length > 0 ? recoveredMessages : snapshot.messages,
|
|
488
|
+
};
|
|
454
489
|
this.storage.saveSession(updated);
|
|
455
490
|
this.sessions.set(snapshot.id, {
|
|
456
491
|
...updated,
|
|
492
|
+
provider,
|
|
457
493
|
processId: null,
|
|
458
494
|
ptyProcess: null,
|
|
459
495
|
stopRequested: false,
|
|
460
496
|
confirmWindow: "",
|
|
461
497
|
ptyPermissionBlocked: false,
|
|
462
498
|
lastAutoConfirmAt: 0,
|
|
463
|
-
autoApprovePermissions: this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode),
|
|
499
|
+
autoApprovePermissions: this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode, provider),
|
|
464
500
|
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
465
501
|
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
466
502
|
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
@@ -487,13 +523,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
487
523
|
else {
|
|
488
524
|
this.sessions.set(snapshot.id, {
|
|
489
525
|
...snapshot,
|
|
526
|
+
provider,
|
|
490
527
|
processId: null,
|
|
491
528
|
ptyProcess: null,
|
|
492
529
|
stopRequested: false,
|
|
493
530
|
confirmWindow: "",
|
|
494
531
|
ptyPermissionBlocked: false,
|
|
495
532
|
lastAutoConfirmAt: 0,
|
|
496
|
-
autoApprovePermissions: this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode),
|
|
533
|
+
autoApprovePermissions: this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode, provider),
|
|
497
534
|
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
498
535
|
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
499
536
|
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
@@ -565,30 +602,40 @@ export class ProcessManager extends EventEmitter {
|
|
|
565
602
|
this.deleteClaudeCache(record);
|
|
566
603
|
}
|
|
567
604
|
this.sessions.delete(id);
|
|
568
|
-
this.
|
|
605
|
+
this.lastPersistedMessageState.delete(id);
|
|
569
606
|
this.storage.deleteSession(id);
|
|
570
607
|
}
|
|
571
608
|
}
|
|
572
609
|
start(command, cwd, mode, initialInput, opts) {
|
|
573
610
|
this.assertCommandAllowed(command);
|
|
574
|
-
const
|
|
611
|
+
const baseCwd = cwd
|
|
575
612
|
? path.resolve(process.cwd(), cwd)
|
|
576
613
|
: path.resolve(process.cwd(), this.config.defaultCwd);
|
|
577
|
-
const isClaudeCmd = this.isClaudeCommand(command);
|
|
578
|
-
// For full-access mode with claude, add permission flags
|
|
579
|
-
const processedCommand = this.processCommandForMode(command, mode);
|
|
580
|
-
const resumeCommandSessionId = getResumeCommandSessionId(processedCommand) ?? getResumeCommandSessionId(command);
|
|
581
|
-
const knownClaudeTaskIds = isClaudeCmd ? new Set(listRecentClaudeProjectSessionIds(resolvedCwd, new Date().toISOString())) : null;
|
|
582
|
-
const knownClaudeProjectMtimes = isClaudeCmd ? listClaudeProjectSessionMtimes(resolvedCwd) : null;
|
|
583
|
-
const initialClaudeSessionId = resumeCommandSessionId ?? null;
|
|
584
|
-
const startedAt = new Date().toISOString();
|
|
585
614
|
const id = randomUUID();
|
|
615
|
+
const worktreeSetup = opts?.worktreeEnabled
|
|
616
|
+
? prepareSessionWorktree({ cwd: baseCwd, sessionId: id })
|
|
617
|
+
: null;
|
|
618
|
+
const resolvedCwd = worktreeSetup?.cwd ?? baseCwd;
|
|
619
|
+
const provider = opts?.provider ?? resolveProviderFromCommand(command);
|
|
620
|
+
const effectiveMode = provider === "codex" ? "full-access" : mode;
|
|
621
|
+
const isClaudeProvider = provider === "claude";
|
|
622
|
+
const processedCommand = this.processCommandForMode(command, effectiveMode, provider);
|
|
623
|
+
const resumeCommandSessionId = isClaudeProvider
|
|
624
|
+
? getResumeCommandSessionId(processedCommand) ?? getResumeCommandSessionId(command)
|
|
625
|
+
: null;
|
|
626
|
+
const knownClaudeTaskIds = isClaudeProvider ? new Set(listRecentClaudeProjectSessionIds(resolvedCwd, new Date().toISOString())) : null;
|
|
627
|
+
const knownClaudeProjectMtimes = isClaudeProvider ? listClaudeProjectSessionMtimes(resolvedCwd) : null;
|
|
628
|
+
const initialClaudeSessionId = isClaudeProvider ? resumeCommandSessionId ?? null : null;
|
|
629
|
+
const startedAt = new Date().toISOString();
|
|
586
630
|
const record = {
|
|
587
631
|
id,
|
|
632
|
+
provider,
|
|
588
633
|
command,
|
|
589
634
|
cwd: resolvedCwd,
|
|
590
|
-
mode,
|
|
591
|
-
|
|
635
|
+
mode: effectiveMode,
|
|
636
|
+
worktreeEnabled: Boolean(worktreeSetup),
|
|
637
|
+
worktree: worktreeSetup?.worktree ?? null,
|
|
638
|
+
autonomyPolicy: this.defaultAutonomyPolicy(effectiveMode),
|
|
592
639
|
approvalPolicy: "ask-every-time",
|
|
593
640
|
allowedScopes: [],
|
|
594
641
|
status: "running",
|
|
@@ -608,7 +655,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
608
655
|
confirmWindow: "",
|
|
609
656
|
ptyPermissionBlocked: false,
|
|
610
657
|
lastAutoConfirmAt: 0,
|
|
611
|
-
autoApprovePermissions: this.shouldAutoApprovePermissions(command,
|
|
658
|
+
autoApprovePermissions: this.shouldAutoApprovePermissions(command, effectiveMode, provider),
|
|
612
659
|
resumedFromSessionId: opts?.resumedFromSessionId ?? null,
|
|
613
660
|
autoRecovered: opts?.autoRecovered ?? false,
|
|
614
661
|
rememberedEscalationScopes: new Set(),
|
|
@@ -625,22 +672,21 @@ export class ProcessManager extends EventEmitter {
|
|
|
625
672
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
626
673
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
627
674
|
};
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
675
|
+
if (isClaudeProvider) {
|
|
676
|
+
record.ptyBridge = new ClaudePtyBridge({
|
|
677
|
+
sessionId: id,
|
|
678
|
+
isClaudeCommand: true,
|
|
679
|
+
autoApprove: record.autoApprovePermissions,
|
|
680
|
+
approvalPolicy: record.approvalPolicy,
|
|
681
|
+
});
|
|
682
|
+
record.ptyBridge.on("event", (event) => {
|
|
683
|
+
this.handleBridgeEvent(record, event);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
638
686
|
this.sessions.set(id, record);
|
|
639
687
|
this.persist(record);
|
|
640
688
|
this.cleanupOldSessions();
|
|
641
|
-
// Register lifecycle
|
|
642
689
|
this.lifecycleManager.register(id, "initializing");
|
|
643
|
-
// All modes use PTY execution — JSON turns are only used for internal recovery
|
|
644
690
|
const shellArgs = this.buildShellArgs(processedCommand);
|
|
645
691
|
let child;
|
|
646
692
|
try {
|
|
@@ -648,9 +694,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
648
694
|
cwd: resolvedCwd,
|
|
649
695
|
env: {
|
|
650
696
|
...process.env,
|
|
651
|
-
WAND_MODE:
|
|
652
|
-
WAND_AUTO_CONFIRM:
|
|
653
|
-
WAND_AUTO_EDIT:
|
|
697
|
+
WAND_MODE: effectiveMode,
|
|
698
|
+
WAND_AUTO_CONFIRM: effectiveMode === "full-access" ? "1" : "0",
|
|
699
|
+
WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0"
|
|
654
700
|
},
|
|
655
701
|
name: "xterm-color",
|
|
656
702
|
cols: 120,
|
|
@@ -671,10 +717,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
671
717
|
record.ptyProcess = child;
|
|
672
718
|
record.status = "running";
|
|
673
719
|
this.lifecycleManager.setState(id, "running");
|
|
674
|
-
// Register exit handler AFTER ptyProcess is assigned — node-pty's EventEmitter
|
|
675
|
-
// fires 'exit' synchronously when the child has already exited (e.g. "command
|
|
676
|
-
// not found"). If we register first, onExit fires with ptyProcess still null and
|
|
677
|
-
// status never updates. By assigning first, onExit always sees a consistent state.
|
|
678
720
|
child.onExit(({ exitCode }) => {
|
|
679
721
|
const current = this.sessions.get(id);
|
|
680
722
|
if (!current)
|
|
@@ -699,14 +741,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
699
741
|
current.ptyProcess = null;
|
|
700
742
|
this.lifecycleManager.archive(id, `Session ${current.status}`, current.stopRequested ? "user" : "error");
|
|
701
743
|
this.flushPersist(current);
|
|
702
|
-
// Final full snapshot with messages to SQLite (persist() only saves metadata)
|
|
703
744
|
this.storage.saveSession(this.snapshot(current));
|
|
704
745
|
this.emitEvent({ type: "ended", sessionId: id, data: this.snapshot(current) });
|
|
705
746
|
});
|
|
706
|
-
// Set PTY write function for bridge (for permission approval).
|
|
707
|
-
// Write directly to record.ptyProcess — the status guard in sendInput() already
|
|
708
|
-
// ensures no input is sent when the session is not running, so we just guard
|
|
709
|
-
// the PTY write itself against a null process.
|
|
710
747
|
if (record.ptyBridge) {
|
|
711
748
|
record.ptyBridge.setPtyWrite((input) => {
|
|
712
749
|
if (record.ptyProcess) {
|
|
@@ -714,7 +751,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
714
751
|
}
|
|
715
752
|
});
|
|
716
753
|
}
|
|
717
|
-
// Emit started event AFTER PTY is fully set up so clients receive a consistent snapshot.
|
|
718
754
|
this.emitEvent({ type: "started", sessionId: id, data: this.snapshot(record) });
|
|
719
755
|
let initialInputSent = false;
|
|
720
756
|
const sendInitialInput = () => {
|
|
@@ -727,27 +763,35 @@ export class ProcessManager extends EventEmitter {
|
|
|
727
763
|
return;
|
|
728
764
|
}
|
|
729
765
|
process.stderr.write(`[wand] Sending initial input (${initialInput.length} chars)\n`);
|
|
730
|
-
// Track initial input via bridge for Chat mode
|
|
731
766
|
if (current.ptyBridge) {
|
|
732
767
|
current.ptyBridge.onUserInput(initialInput);
|
|
733
768
|
}
|
|
734
769
|
current.ptyProcess.write(initialInput);
|
|
735
|
-
// \n advances to a new line so subsequent output doesn't overwrite this input
|
|
736
770
|
current.ptyProcess.write("\n");
|
|
737
771
|
};
|
|
738
772
|
child.onData((chunk) => {
|
|
739
773
|
const rec = this.sessions.get(id);
|
|
740
774
|
if (!rec)
|
|
741
775
|
return;
|
|
742
|
-
// Route chunk through PTY bridge
|
|
743
776
|
if (rec.ptyBridge) {
|
|
744
777
|
rec.ptyBridge.processChunk(chunk);
|
|
778
|
+
rec.output = rec.ptyBridge.getRawOutput();
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
rec.output = appendWindow(rec.output, chunk, 200_000);
|
|
745
782
|
}
|
|
746
|
-
// Update legacy output field for backward compatibility
|
|
747
|
-
rec.output = rec.ptyBridge?.getRawOutput() ?? "";
|
|
748
|
-
// Log raw PTY output for analysis
|
|
749
783
|
this.logger.appendPtyOutput(id, chunk);
|
|
750
|
-
|
|
784
|
+
if (!rec.ptyBridge) {
|
|
785
|
+
this.emitEvent({
|
|
786
|
+
type: "output",
|
|
787
|
+
sessionId: id,
|
|
788
|
+
data: {
|
|
789
|
+
chunk,
|
|
790
|
+
output: rec.output,
|
|
791
|
+
permissionBlocked: this.isPermissionBlocked(rec),
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
}
|
|
751
795
|
const bridgeSessionId = rec.ptyBridge?.getClaudeSessionId();
|
|
752
796
|
if (bridgeSessionId && bridgeSessionId !== rec.claudeSessionId) {
|
|
753
797
|
rec.claudeSessionId = bridgeSessionId;
|
|
@@ -767,8 +811,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
767
811
|
process.stderr.write(`[wand] Captured Claude project session ID: ${discoveredTaskId}\n`);
|
|
768
812
|
}
|
|
769
813
|
}
|
|
770
|
-
|
|
771
|
-
if (rec.autoApprovePermissions && !rec.ptyBridge) {
|
|
814
|
+
if (rec.autoApprovePermissions && !rec.ptyBridge && rec.provider === "claude") {
|
|
772
815
|
this.autoConfirmWithRecord(rec, chunk, child);
|
|
773
816
|
}
|
|
774
817
|
if (initialInput && !initialInputSent && chunk.includes("❯")) {
|
|
@@ -891,6 +934,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
891
934
|
}
|
|
892
935
|
return this.snapshot(record);
|
|
893
936
|
}
|
|
937
|
+
getPtyTranscript(id) {
|
|
938
|
+
return this.logger.readPtyOutput(id);
|
|
939
|
+
}
|
|
894
940
|
sendInput(id, input, view, shortcutKey) {
|
|
895
941
|
const record = this.mustGet(id);
|
|
896
942
|
if (record.status !== "running") {
|
|
@@ -1059,7 +1105,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1059
1105
|
this.logger.deleteSession(id);
|
|
1060
1106
|
this.deleteClaudeCache(record);
|
|
1061
1107
|
this.sessions.delete(id);
|
|
1062
|
-
this.
|
|
1108
|
+
this.lastPersistedMessageState.delete(id);
|
|
1063
1109
|
this.lifecycleManager.unregister(id);
|
|
1064
1110
|
}
|
|
1065
1111
|
deleteClaudeCache(record) {
|
|
@@ -1097,10 +1143,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
1097
1143
|
return {
|
|
1098
1144
|
id: record.id,
|
|
1099
1145
|
sessionKind: "pty",
|
|
1146
|
+
provider: record.provider,
|
|
1100
1147
|
runner: "pty",
|
|
1101
1148
|
command: record.command,
|
|
1102
1149
|
cwd: record.cwd,
|
|
1103
1150
|
mode: record.mode,
|
|
1151
|
+
worktreeEnabled: record.worktreeEnabled ?? false,
|
|
1152
|
+
worktree: record.worktree ?? null,
|
|
1104
1153
|
autonomyPolicy: record.autonomyPolicy,
|
|
1105
1154
|
approvalPolicy: record.approvalPolicy,
|
|
1106
1155
|
allowedScopes: record.allowedScopes,
|
|
@@ -1198,8 +1247,17 @@ export class ProcessManager extends EventEmitter {
|
|
|
1198
1247
|
if (messages !== record.messages) {
|
|
1199
1248
|
record.messages = messages;
|
|
1200
1249
|
}
|
|
1201
|
-
|
|
1202
|
-
this.
|
|
1250
|
+
const snapshot = this.snapshot(record);
|
|
1251
|
+
const shouldSaveMessages = shouldPersistMessages(this.lastPersistedMessageState.get(record.id), messages);
|
|
1252
|
+
// Persist full messages as soon as the structured conversation changes so
|
|
1253
|
+
// service restarts cannot roll the session back to an older tail message.
|
|
1254
|
+
if (shouldSaveMessages) {
|
|
1255
|
+
this.storage.saveSession(snapshot);
|
|
1256
|
+
this.lastPersistedMessageState.set(record.id, getPersistedMessageState(messages));
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
this.storage.saveSessionMetadata(snapshot);
|
|
1260
|
+
}
|
|
1203
1261
|
this.logger.saveMetadata(record.id, {
|
|
1204
1262
|
id: record.id,
|
|
1205
1263
|
command: record.command,
|
|
@@ -1211,13 +1269,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1211
1269
|
resumedToSessionId: record.resumedToSessionId ?? null,
|
|
1212
1270
|
autoRecovered: record.autoRecovered ?? false,
|
|
1213
1271
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const lastCount = this.lastPersistedMessageCount.get(record.id) ?? 0;
|
|
1217
|
-
if (messages.length !== lastCount) {
|
|
1218
|
-
this.lastPersistedMessageCount.set(record.id, messages.length);
|
|
1219
|
-
this.logger.saveMessages(record.id, messages);
|
|
1220
|
-
}
|
|
1272
|
+
if (shouldSaveMessages) {
|
|
1273
|
+
this.logger.saveMessages(record.id, messages);
|
|
1221
1274
|
}
|
|
1222
1275
|
}
|
|
1223
1276
|
/**
|
|
@@ -1526,11 +1579,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
1526
1579
|
// immediately with "command not found" — a silent exit before onExit is ready.
|
|
1527
1580
|
return ["-lc", command];
|
|
1528
1581
|
}
|
|
1529
|
-
shouldAutoApprovePermissions(command, mode) {
|
|
1582
|
+
shouldAutoApprovePermissions(command, mode, provider) {
|
|
1583
|
+
if (provider !== "claude") {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1530
1586
|
if (!/^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command)) {
|
|
1531
1587
|
return false;
|
|
1532
1588
|
}
|
|
1533
|
-
// Root mode: always auto-approve (Claude CLI refuses --permission-mode bypassPermissions under root)
|
|
1534
1589
|
if (isRunningAsRoot()) {
|
|
1535
1590
|
return true;
|
|
1536
1591
|
}
|
|
@@ -1542,26 +1597,29 @@ export class ProcessManager extends EventEmitter {
|
|
|
1542
1597
|
}
|
|
1543
1598
|
return false;
|
|
1544
1599
|
}
|
|
1545
|
-
processCommandForMode(command, mode) {
|
|
1600
|
+
processCommandForMode(command, mode, provider) {
|
|
1601
|
+
if (provider === "codex") {
|
|
1602
|
+
if (mode !== "full-access") {
|
|
1603
|
+
return command;
|
|
1604
|
+
}
|
|
1605
|
+
if (/--dangerously-bypass-approvals-and-sandbox(?:\s|$)/.test(command)) {
|
|
1606
|
+
return command;
|
|
1607
|
+
}
|
|
1608
|
+
return `${command} --dangerously-bypass-approvals-and-sandbox`;
|
|
1609
|
+
}
|
|
1546
1610
|
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
1547
1611
|
if (!isClaudeCmd)
|
|
1548
1612
|
return command;
|
|
1549
1613
|
let result = command;
|
|
1550
|
-
// Skip if command already contains --permission-mode
|
|
1551
1614
|
const hasPermFlag = /--permission-mode\s/.test(command);
|
|
1552
1615
|
if (!hasPermFlag) {
|
|
1553
1616
|
if (isRunningAsRoot()) {
|
|
1554
|
-
// Root: Claude CLI refuses --permission-mode bypassPermissions.
|
|
1555
|
-
// Use acceptEdits + --allowedTools to auto-approve all tool calls
|
|
1556
|
-
// regardless of whether the target path is inside or outside the CWD.
|
|
1557
1617
|
if (mode === "managed" || mode === "full-access" || mode === "auto-edit") {
|
|
1558
1618
|
result += " --permission-mode acceptEdits";
|
|
1559
1619
|
result += " --allowedTools Bash Edit Write Read Glob Grep NotebookEdit WebFetch WebSearch";
|
|
1560
1620
|
}
|
|
1561
1621
|
}
|
|
1562
1622
|
else {
|
|
1563
|
-
// Non-root: use bypassPermissions for full-access (skips all prompts),
|
|
1564
|
-
// acceptEdits for auto-edit (auto-accepts file writes, prompts for bash).
|
|
1565
1623
|
if (mode === "full-access" || mode === "managed") {
|
|
1566
1624
|
result += " --permission-mode bypassPermissions";
|
|
1567
1625
|
}
|
|
@@ -1570,15 +1628,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
1570
1628
|
}
|
|
1571
1629
|
}
|
|
1572
1630
|
}
|
|
1573
|
-
// In managed mode, append a system prompt instructing Claude to act autonomously
|
|
1574
|
-
// without asking the user for confirmation, since the user may not be monitoring.
|
|
1575
1631
|
if (mode === "managed") {
|
|
1576
1632
|
const autonomousPrompt = "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
1577
|
-
// Escape single quotes for shell safety
|
|
1578
1633
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1579
1634
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1580
1635
|
}
|
|
1581
|
-
// Append language preference if configured
|
|
1582
1636
|
const language = this.config.language?.trim();
|
|
1583
1637
|
if (language) {
|
|
1584
1638
|
const langPrompt = `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`;
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -6,64 +6,114 @@
|
|
|
6
6
|
/** Strip ANSI escape sequences and control characters from raw PTY output. */
|
|
7
7
|
export function stripAnsi(text) {
|
|
8
8
|
return text
|
|
9
|
-
.replace(/\x1b\[
|
|
10
|
-
.replace(/\x1b\
|
|
11
|
-
.replace(/\x1b[
|
|
9
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
10
|
+
.replace(/\x1b\[(\d+)C/g, (_match, count) => " ".repeat(Number(count) || 1))
|
|
11
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
|
|
12
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
13
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
|
|
14
|
+
.replace(/\x1bM/g, "\n")
|
|
15
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
|
|
16
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
17
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
18
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
12
19
|
// eslint-disable-next-line no-control-regex
|
|
13
|
-
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
14
|
-
.replace(/\r\n?/g, "\n")
|
|
20
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
21
|
+
.replace(/\r\n?/g, "\n")
|
|
22
|
+
.replace(/[\t ]+\n/g, "\n")
|
|
23
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
15
24
|
}
|
|
16
25
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
|
17
26
|
export function isNoiseLine(line) {
|
|
18
27
|
if (!line)
|
|
19
28
|
return false;
|
|
20
|
-
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return false;
|
|
32
|
+
if (trimmed.startsWith("────"))
|
|
33
|
+
return true;
|
|
34
|
+
if (trimmed === "❯" || trimmed === "›")
|
|
35
|
+
return true;
|
|
36
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]{2,}$/.test(trimmed))
|
|
37
|
+
return true;
|
|
38
|
+
if (/^[▁▂▃▄▅▆▇█▔▕▏▐]+$/.test(trimmed))
|
|
39
|
+
return true;
|
|
40
|
+
if (trimmed.includes("esc to interrupt"))
|
|
41
|
+
return true;
|
|
42
|
+
if (trimmed.includes("Claude Code v"))
|
|
43
|
+
return true;
|
|
44
|
+
if (/^Sonnet\b/.test(trimmed))
|
|
45
|
+
return true;
|
|
46
|
+
if (trimmed.includes("Failed to install Anthropic"))
|
|
47
|
+
return true;
|
|
48
|
+
if (trimmed.includes("Claude Code has switched"))
|
|
49
|
+
return true;
|
|
50
|
+
if (trimmed.includes("? for shortcuts"))
|
|
51
|
+
return true;
|
|
52
|
+
if (trimmed.includes("Claude is waiting"))
|
|
53
|
+
return true;
|
|
54
|
+
if (trimmed.includes("[wand]"))
|
|
55
|
+
return true;
|
|
56
|
+
if (trimmed.startsWith("0;") || trimmed.startsWith("9;"))
|
|
57
|
+
return true;
|
|
58
|
+
if (trimmed.includes("ctrl+g"))
|
|
59
|
+
return true;
|
|
60
|
+
if (trimmed.includes("/effort"))
|
|
61
|
+
return true;
|
|
62
|
+
if (/^Using .* for .* session/.test(trimmed))
|
|
63
|
+
return true;
|
|
64
|
+
if (trimmed.startsWith("Press ") && trimmed.includes(" for"))
|
|
65
|
+
return true;
|
|
66
|
+
if (trimmed.startsWith("type ") && trimmed.includes(" to "))
|
|
67
|
+
return true;
|
|
68
|
+
if (trimmed.includes("auto mode is unavailable"))
|
|
69
|
+
return true;
|
|
70
|
+
if (/MCP server.*failed/i.test(trimmed))
|
|
21
71
|
return true;
|
|
22
|
-
if (
|
|
72
|
+
if (trimmed.includes("Germinating") || trimmed.includes("Doodling") || trimmed.includes("Brewing"))
|
|
23
73
|
return true;
|
|
24
|
-
if (
|
|
74
|
+
if (trimmed.includes("Permissions") && trimmed.includes("mode"))
|
|
25
75
|
return true;
|
|
26
|
-
if (
|
|
76
|
+
if (trimmed.startsWith("●") && trimmed.includes("·"))
|
|
27
77
|
return true;
|
|
28
|
-
if (
|
|
78
|
+
if (trimmed.startsWith("[>") || trimmed.startsWith("[<"))
|
|
29
79
|
return true;
|
|
30
|
-
if (
|
|
80
|
+
if (trimmed.includes("Captured Claude session ID"))
|
|
31
81
|
return true;
|
|
32
|
-
if (
|
|
82
|
+
if (/^>_\s*OpenAI Codex\b/.test(trimmed))
|
|
33
83
|
return true;
|
|
34
|
-
if (
|
|
84
|
+
if (/^OpenAI Codex\b/i.test(trimmed))
|
|
35
85
|
return true;
|
|
36
|
-
if (
|
|
86
|
+
if (/^(model|directory):\s+/i.test(trimmed))
|
|
37
87
|
return true;
|
|
38
|
-
if (
|
|
88
|
+
if (/^(tip|context):\s+/i.test(trimmed))
|
|
39
89
|
return true;
|
|
40
|
-
if (
|
|
90
|
+
if (/^work(tree|space):\s+/i.test(trimmed))
|
|
41
91
|
return true;
|
|
42
|
-
if (
|
|
92
|
+
if (/^(approvals?|sandbox|provider|session id):\s+/i.test(trimmed))
|
|
43
93
|
return true;
|
|
44
|
-
if (
|
|
94
|
+
if (/^(thinking|working)(\.\.\.|…)?$/i.test(trimmed))
|
|
45
95
|
return true;
|
|
46
|
-
if (/^
|
|
96
|
+
if (/^[•◦·]\s+Working\b/i.test(trimmed))
|
|
47
97
|
return true;
|
|
48
|
-
if (
|
|
98
|
+
if (/^[•◦·]\s+(Running|Planning|Applying|Reading|Searching)\b/i.test(trimmed))
|
|
49
99
|
return true;
|
|
50
|
-
if (
|
|
100
|
+
if (/^[•◦·]\s+(Inspecting|Reviewing|Summarizing|Editing|Updating|Writing)\b/i.test(trimmed))
|
|
51
101
|
return true;
|
|
52
|
-
if (
|
|
102
|
+
if (/^[•◦·]\s+Completed\b/i.test(trimmed))
|
|
53
103
|
return true;
|
|
54
|
-
if (
|
|
104
|
+
if (/^(ctrl|enter|tab|shift|esc|alt)\+/i.test(trimmed))
|
|
55
105
|
return true;
|
|
56
|
-
if (
|
|
106
|
+
if (/\b(open|close|toggle) (chat|terminal)\b/i.test(trimmed))
|
|
57
107
|
return true;
|
|
58
|
-
if (
|
|
108
|
+
if (/\b(approve|deny)\b.*\b(permission|approval)\b/i.test(trimmed))
|
|
59
109
|
return true;
|
|
60
|
-
if (
|
|
110
|
+
if (/^(use|press) .* (to|for) .*/i.test(trimmed))
|
|
61
111
|
return true;
|
|
62
|
-
if (
|
|
112
|
+
if (/^(?:token|context window|remaining context|conversation):\s+/i.test(trimmed))
|
|
63
113
|
return true;
|
|
64
|
-
if (
|
|
114
|
+
if (/^(?:cwd|path):\s+\//i.test(trimmed))
|
|
65
115
|
return true;
|
|
66
|
-
if (
|
|
116
|
+
if (/^[<>│┆╎].*[<>│┆╎]$/.test(trimmed) && trimmed.length < 8)
|
|
67
117
|
return true;
|
|
68
118
|
return false;
|
|
69
119
|
}
|