@love-moon/tui-driver 0.2.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.
Files changed (113) hide show
  1. package/README.md +142 -0
  2. package/dist/driver/StateMachine.d.ts +28 -0
  3. package/dist/driver/StateMachine.d.ts.map +1 -0
  4. package/dist/driver/StateMachine.js +56 -0
  5. package/dist/driver/StateMachine.js.map +1 -0
  6. package/dist/driver/TuiDriver.d.ts +73 -0
  7. package/dist/driver/TuiDriver.d.ts.map +1 -0
  8. package/dist/driver/TuiDriver.js +506 -0
  9. package/dist/driver/TuiDriver.js.map +1 -0
  10. package/dist/driver/TuiProfile.d.ts +59 -0
  11. package/dist/driver/TuiProfile.d.ts.map +1 -0
  12. package/dist/driver/TuiProfile.js +13 -0
  13. package/dist/driver/TuiProfile.js.map +1 -0
  14. package/dist/driver/index.d.ts +5 -0
  15. package/dist/driver/index.d.ts.map +1 -0
  16. package/dist/driver/index.js +13 -0
  17. package/dist/driver/index.js.map +1 -0
  18. package/dist/driver/profiles/claudeCode.profile.d.ts +4 -0
  19. package/dist/driver/profiles/claudeCode.profile.d.ts.map +1 -0
  20. package/dist/driver/profiles/claudeCode.profile.js +91 -0
  21. package/dist/driver/profiles/claudeCode.profile.js.map +1 -0
  22. package/dist/driver/profiles/codex.profile.d.ts +4 -0
  23. package/dist/driver/profiles/codex.profile.d.ts.map +1 -0
  24. package/dist/driver/profiles/codex.profile.js +82 -0
  25. package/dist/driver/profiles/codex.profile.js.map +1 -0
  26. package/dist/driver/profiles/index.d.ts +3 -0
  27. package/dist/driver/profiles/index.d.ts.map +1 -0
  28. package/dist/driver/profiles/index.js +8 -0
  29. package/dist/driver/profiles/index.js.map +1 -0
  30. package/dist/example.d.ts +2 -0
  31. package/dist/example.d.ts.map +1 -0
  32. package/dist/example.js +43 -0
  33. package/dist/example.js.map +1 -0
  34. package/dist/expect/ExpectEngine.d.ts +34 -0
  35. package/dist/expect/ExpectEngine.d.ts.map +1 -0
  36. package/dist/expect/ExpectEngine.js +121 -0
  37. package/dist/expect/ExpectEngine.js.map +1 -0
  38. package/dist/expect/Matchers.d.ts +24 -0
  39. package/dist/expect/Matchers.d.ts.map +1 -0
  40. package/dist/expect/Matchers.js +71 -0
  41. package/dist/expect/Matchers.js.map +1 -0
  42. package/dist/expect/index.d.ts +3 -0
  43. package/dist/expect/index.d.ts.map +1 -0
  44. package/dist/expect/index.js +8 -0
  45. package/dist/expect/index.js.map +1 -0
  46. package/dist/extract/Diff.d.ts +10 -0
  47. package/dist/extract/Diff.d.ts.map +1 -0
  48. package/dist/extract/Diff.js +44 -0
  49. package/dist/extract/Diff.js.map +1 -0
  50. package/dist/extract/OutputExtractor.d.ts +16 -0
  51. package/dist/extract/OutputExtractor.d.ts.map +1 -0
  52. package/dist/extract/OutputExtractor.js +71 -0
  53. package/dist/extract/OutputExtractor.js.map +1 -0
  54. package/dist/extract/index.d.ts +3 -0
  55. package/dist/extract/index.d.ts.map +1 -0
  56. package/dist/extract/index.js +11 -0
  57. package/dist/extract/index.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +50 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/pty/PtySession.d.ts +38 -0
  63. package/dist/pty/PtySession.d.ts.map +1 -0
  64. package/dist/pty/PtySession.js +231 -0
  65. package/dist/pty/PtySession.js.map +1 -0
  66. package/dist/pty/index.d.ts +2 -0
  67. package/dist/pty/index.d.ts.map +1 -0
  68. package/dist/pty/index.js +6 -0
  69. package/dist/pty/index.js.map +1 -0
  70. package/dist/term/HeadlessScreen.d.ts +29 -0
  71. package/dist/term/HeadlessScreen.d.ts.map +1 -0
  72. package/dist/term/HeadlessScreen.js +126 -0
  73. package/dist/term/HeadlessScreen.js.map +1 -0
  74. package/dist/term/ScreenSnapshot.d.ts +37 -0
  75. package/dist/term/ScreenSnapshot.d.ts.map +1 -0
  76. package/dist/term/ScreenSnapshot.js +68 -0
  77. package/dist/term/ScreenSnapshot.js.map +1 -0
  78. package/dist/term/index.d.ts +3 -0
  79. package/dist/term/index.d.ts.map +1 -0
  80. package/dist/term/index.js +8 -0
  81. package/dist/term/index.js.map +1 -0
  82. package/docs/tui-driver_implementation_plan.md +307 -0
  83. package/package.json +33 -0
  84. package/pnpm-workspace.yaml +1 -0
  85. package/src/driver/StateMachine.ts +90 -0
  86. package/src/driver/TuiDriver.ts +624 -0
  87. package/src/driver/TuiProfile.ts +72 -0
  88. package/src/driver/index.ts +4 -0
  89. package/src/driver/profiles/claudeCode.profile.ts +96 -0
  90. package/src/driver/profiles/codex.profile.ts +87 -0
  91. package/src/driver/profiles/index.ts +2 -0
  92. package/src/example.ts +45 -0
  93. package/src/expect/ExpectEngine.ts +171 -0
  94. package/src/expect/Matchers.ts +92 -0
  95. package/src/expect/index.ts +2 -0
  96. package/src/extract/Diff.ts +51 -0
  97. package/src/extract/OutputExtractor.ts +88 -0
  98. package/src/extract/index.ts +2 -0
  99. package/src/index.ts +67 -0
  100. package/src/pty/PtySession.ts +234 -0
  101. package/src/pty/index.ts +1 -0
  102. package/src/term/HeadlessScreen.ts +151 -0
  103. package/src/term/ScreenSnapshot.ts +89 -0
  104. package/src/term/index.ts +2 -0
  105. package/test/claude-profile.test.ts +11 -0
  106. package/test/codex-profile.test.ts +108 -0
  107. package/test/debug-claude.ts +51 -0
  108. package/test/integration.ts +174 -0
  109. package/test/output-extractor.test.ts +49 -0
  110. package/test/state-diff.test.ts +120 -0
  111. package/test/unit.test.ts +136 -0
  112. package/tsconfig.json +20 -0
  113. package/vitest.config.ts +13 -0
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeadlessScreen = void 0;
4
+ const headless_1 = require("@xterm/headless");
5
+ const addon_serialize_1 = require("@xterm/addon-serialize");
6
+ const ScreenSnapshot_js_1 = require("./ScreenSnapshot.js");
7
+ const DEFAULT_COLS = 120;
8
+ const DEFAULT_ROWS = 40;
9
+ const DEFAULT_SCROLLBACK = 5000;
10
+ class HeadlessScreen {
11
+ terminal;
12
+ serializeAddon;
13
+ _cols;
14
+ _rows;
15
+ onTerminalReply;
16
+ constructor(options = {}) {
17
+ this._cols = options.cols ?? DEFAULT_COLS;
18
+ this._rows = options.rows ?? DEFAULT_ROWS;
19
+ this.onTerminalReply = options.onTerminalReply;
20
+ this.terminal = new headless_1.Terminal({
21
+ cols: this._cols,
22
+ rows: this._rows,
23
+ scrollback: options.scrollback ?? DEFAULT_SCROLLBACK,
24
+ allowProposedApi: true,
25
+ });
26
+ this.serializeAddon = new addon_serialize_1.SerializeAddon();
27
+ this.terminal.loadAddon(this.serializeAddon);
28
+ }
29
+ get cols() {
30
+ return this._cols;
31
+ }
32
+ get rows() {
33
+ return this._rows;
34
+ }
35
+ write(data) {
36
+ const replies = this.getDsrReplies(data);
37
+ this.terminal.write(data);
38
+ if (this.onTerminalReply && replies.length > 0) {
39
+ for (const reply of replies) {
40
+ this.onTerminalReply(reply);
41
+ }
42
+ }
43
+ }
44
+ resize(cols, rows) {
45
+ this._cols = cols;
46
+ this._rows = rows;
47
+ this.terminal.resize(cols, rows);
48
+ }
49
+ clear() {
50
+ this.terminal.clear();
51
+ }
52
+ reset() {
53
+ this.terminal.reset();
54
+ }
55
+ snapshot() {
56
+ const viewportText = this.getViewportText();
57
+ const scrollbackText = this.getScrollbackText();
58
+ const cursor = this.getCursor();
59
+ const hash = ScreenSnapshot_js_1.ScreenSnapshot.computeHash(viewportText);
60
+ return new ScreenSnapshot_js_1.ScreenSnapshot({
61
+ viewportText,
62
+ scrollbackText,
63
+ cursor,
64
+ hash,
65
+ timestamp: Date.now(),
66
+ cols: this._cols,
67
+ rows: this._rows,
68
+ });
69
+ }
70
+ getViewportText() {
71
+ const buffer = this.terminal.buffer.active;
72
+ const lines = [];
73
+ for (let i = 0; i < this._rows; i++) {
74
+ const line = buffer.getLine(buffer.baseY + i);
75
+ if (line) {
76
+ lines.push(line.translateToString(true));
77
+ }
78
+ else {
79
+ lines.push("");
80
+ }
81
+ }
82
+ return lines.join("\n");
83
+ }
84
+ getScrollbackText() {
85
+ const buffer = this.terminal.buffer.active;
86
+ const lines = [];
87
+ const totalLines = buffer.baseY + this._rows;
88
+ for (let i = 0; i < totalLines; i++) {
89
+ const line = buffer.getLine(i);
90
+ if (line) {
91
+ lines.push(line.translateToString(true));
92
+ }
93
+ else {
94
+ lines.push("");
95
+ }
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+ getCursor() {
100
+ const buffer = this.terminal.buffer.active;
101
+ return {
102
+ x: buffer.cursorX,
103
+ y: buffer.cursorY,
104
+ };
105
+ }
106
+ getDsrReplies(data) {
107
+ const dsrRegex = /\x1b\[(\?)?6n/g;
108
+ const matches = data.match(dsrRegex);
109
+ if (!matches || matches.length === 0) {
110
+ return [];
111
+ }
112
+ const cursor = this.getCursor();
113
+ const row = cursor.y + 1;
114
+ const col = cursor.x + 1;
115
+ const reply = `\x1b[${row};${col}R`;
116
+ return Array.from({ length: matches.length }, () => reply);
117
+ }
118
+ serialize() {
119
+ return this.serializeAddon.serialize();
120
+ }
121
+ dispose() {
122
+ this.terminal.dispose();
123
+ }
124
+ }
125
+ exports.HeadlessScreen = HeadlessScreen;
126
+ //# sourceMappingURL=HeadlessScreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HeadlessScreen.js","sourceRoot":"","sources":["../../src/term/HeadlessScreen.ts"],"names":[],"mappings":";;;AAAA,8CAA2C;AAC3C,4DAAwD;AACxD,2DAAqD;AASrD,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAa,cAAc;IACjB,QAAQ,CAAW;IACnB,cAAc,CAAiB;IAC/B,KAAK,CAAS;IACd,KAAK,CAAS;IACd,eAAe,CAA0B;IAEjD,YAAY,UAAiC,EAAE;QAC7C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC;QAC1C,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;QAE/C,IAAI,CAAC,QAAQ,GAAG,IAAI,mBAAQ,CAAC;YAC3B,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,kBAAkB;YACpD,gBAAgB,EAAE,IAAI;SACvB,CAAC,CAAC;QAEH,IAAI,CAAC,cAAc,GAAG,IAAI,gCAAc,EAAE,CAAC;QAC3C,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,IAAY;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAY,EAAE,IAAY;QAC/B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,QAAQ;QACN,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAC5C,MAAM,cAAc,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,kCAAc,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QAEtD,OAAO,IAAI,kCAAc,CAAC;YACxB,YAAY;YACZ,cAAc;YACd,MAAM;YACN,IAAI;YACJ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,IAAI,EAAE,IAAI,CAAC,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAEO,eAAe;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC9C,IAAI,IAAI,EAAE,CAAC;gBACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEO,iBAAiB;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAE7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC/B,IAAI,IAAI,EAAE,CAAC;gBACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEO,SAAS;QACf,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAC3C,OAAO;YACL,CAAC,EAAE,MAAM,CAAC,OAAO;YACjB,CAAC,EAAE,MAAM,CAAC,OAAO;SAClB,CAAC;IACJ,CAAC;IAEO,aAAa,CAAC,IAAY;QAChC,MAAM,QAAQ,GAAG,gBAAgB,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,GAAG,GAAG,CAAC;QAEpC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAC7D,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC;IACzC,CAAC;IAED,OAAO;QACL,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;CACF;AAvID,wCAuIC"}
@@ -0,0 +1,37 @@
1
+ export interface ScreenSnapshotData {
2
+ viewportText: string;
3
+ scrollbackText: string;
4
+ cursor: {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ hash: string;
9
+ timestamp: number;
10
+ cols: number;
11
+ rows: number;
12
+ }
13
+ export declare class ScreenSnapshot implements ScreenSnapshotData {
14
+ readonly viewportText: string;
15
+ readonly scrollbackText: string;
16
+ readonly cursor: {
17
+ x: number;
18
+ y: number;
19
+ };
20
+ readonly hash: string;
21
+ readonly timestamp: number;
22
+ readonly cols: number;
23
+ readonly rows: number;
24
+ constructor(data: ScreenSnapshotData);
25
+ static computeHash(text: string): string;
26
+ getViewportLines(): string[];
27
+ getScrollbackLines(): string[];
28
+ getViewportWithoutBottom(bottomLines: number): string;
29
+ matchesPattern(pattern: RegExp, region?: "viewport" | "scrollback"): boolean;
30
+ findPattern(pattern: RegExp, region?: "viewport" | "scrollback"): RegExpMatchArray | null;
31
+ diff(other: ScreenSnapshot): {
32
+ added: string[];
33
+ removed: string[];
34
+ };
35
+ toJSON(): ScreenSnapshotData;
36
+ }
37
+ //# sourceMappingURL=ScreenSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScreenSnapshot.d.ts","sourceRoot":"","sources":["../../src/term/ScreenSnapshot.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,cAAe,YAAW,kBAAkB;IACvD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,kBAAkB;IAUpC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIxC,gBAAgB,IAAI,MAAM,EAAE;IAI5B,kBAAkB,IAAI,MAAM,EAAE;IAI9B,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAQrD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,UAAU,GAAG,YAAyB,GAAG,OAAO;IAKxF,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,UAAU,GAAG,YAAyB,GAAG,gBAAgB,GAAG,IAAI;IAKrG,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG;QAC3B,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB;IAaD,MAAM,IAAI,kBAAkB;CAW7B"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScreenSnapshot = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class ScreenSnapshot {
6
+ viewportText;
7
+ scrollbackText;
8
+ cursor;
9
+ hash;
10
+ timestamp;
11
+ cols;
12
+ rows;
13
+ constructor(data) {
14
+ this.viewportText = data.viewportText;
15
+ this.scrollbackText = data.scrollbackText;
16
+ this.cursor = data.cursor;
17
+ this.hash = data.hash;
18
+ this.timestamp = data.timestamp;
19
+ this.cols = data.cols;
20
+ this.rows = data.rows;
21
+ }
22
+ static computeHash(text) {
23
+ return (0, crypto_1.createHash)("md5").update(text).digest("hex").slice(0, 16);
24
+ }
25
+ getViewportLines() {
26
+ return this.viewportText.split("\n");
27
+ }
28
+ getScrollbackLines() {
29
+ return this.scrollbackText.split("\n");
30
+ }
31
+ getViewportWithoutBottom(bottomLines) {
32
+ const lines = this.getViewportLines();
33
+ if (bottomLines >= lines.length) {
34
+ return "";
35
+ }
36
+ return lines.slice(0, lines.length - bottomLines).join("\n");
37
+ }
38
+ matchesPattern(pattern, region = "viewport") {
39
+ const text = region === "viewport" ? this.viewportText : this.scrollbackText;
40
+ return pattern.test(text);
41
+ }
42
+ findPattern(pattern, region = "viewport") {
43
+ const text = region === "viewport" ? this.viewportText : this.scrollbackText;
44
+ return text.match(pattern);
45
+ }
46
+ diff(other) {
47
+ const thisLines = this.scrollbackText.split("\n");
48
+ const otherLines = other.scrollbackText.split("\n");
49
+ const thisSet = new Set(thisLines);
50
+ const otherSet = new Set(otherLines);
51
+ const added = otherLines.filter(line => !thisSet.has(line));
52
+ const removed = thisLines.filter(line => !otherSet.has(line));
53
+ return { added, removed };
54
+ }
55
+ toJSON() {
56
+ return {
57
+ viewportText: this.viewportText,
58
+ scrollbackText: this.scrollbackText,
59
+ cursor: this.cursor,
60
+ hash: this.hash,
61
+ timestamp: this.timestamp,
62
+ cols: this.cols,
63
+ rows: this.rows,
64
+ };
65
+ }
66
+ }
67
+ exports.ScreenSnapshot = ScreenSnapshot;
68
+ //# sourceMappingURL=ScreenSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScreenSnapshot.js","sourceRoot":"","sources":["../../src/term/ScreenSnapshot.ts"],"names":[],"mappings":";;;AAAA,mCAAoC;AAYpC,MAAa,cAAc;IAChB,YAAY,CAAS;IACrB,cAAc,CAAS;IACvB,MAAM,CAA2B;IACjC,IAAI,CAAS;IACb,SAAS,CAAS;IAClB,IAAI,CAAS;IACb,IAAI,CAAS;IAEtB,YAAY,IAAwB;QAClC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACtC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED,MAAM,CAAC,WAAW,CAAC,IAAY;QAC7B,OAAO,IAAA,mBAAU,EAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,gBAAgB;QACd,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,kBAAkB;QAChB,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,wBAAwB,CAAC,WAAmB;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtC,IAAI,WAAW,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC;IAED,cAAc,CAAC,OAAe,EAAE,SAAoC,UAAU;QAC5E,MAAM,IAAI,GAAG,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;QAC7E,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,WAAW,CAAC,OAAe,EAAE,SAAoC,UAAU;QACzE,MAAM,IAAI,GAAG,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;QAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,CAAC,KAAqB;QAIxB,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAEpD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;QAErC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAE9D,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC5B,CAAC;IAED,MAAM;QACJ,OAAO;YACL,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC;IACJ,CAAC;CACF;AA5ED,wCA4EC"}
@@ -0,0 +1,3 @@
1
+ export { HeadlessScreen, HeadlessScreenOptions } from "./HeadlessScreen.js";
2
+ export { ScreenSnapshot, ScreenSnapshotData } from "./ScreenSnapshot.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/term/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScreenSnapshot = exports.HeadlessScreen = void 0;
4
+ var HeadlessScreen_js_1 = require("./HeadlessScreen.js");
5
+ Object.defineProperty(exports, "HeadlessScreen", { enumerable: true, get: function () { return HeadlessScreen_js_1.HeadlessScreen; } });
6
+ var ScreenSnapshot_js_1 = require("./ScreenSnapshot.js");
7
+ Object.defineProperty(exports, "ScreenSnapshot", { enumerable: true, get: function () { return ScreenSnapshot_js_1.ScreenSnapshot; } });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/term/index.ts"],"names":[],"mappings":";;;AAAA,yDAA4E;AAAnE,mHAAA,cAAc,OAAA;AACvB,yDAAyE;AAAhE,mHAAA,cAAc,OAAA"}
@@ -0,0 +1,307 @@
1
+ # TUI Driver(状态机 + Screen Buffer + Expect)实现计划(Headless)
2
+ > 目标:使用 **node-pty** 驱动 **Codex / Claude Code** 的交互式 TUI/REPL;利用 **@xterm/headless** 进行 ANSI/终端仿真和 screen buffer 维护;在 headless 模式下通过 “expect + 状态机” 与 TUI 稳定交互;抽取模型输出并返回给上层。
3
+
4
+ ---
5
+
6
+ ## 1. 总体思路(为什么 headless 可行)
7
+
8
+ - `node-pty`:提供伪终端(PTY),可以让交互式 CLI 认为自己在真实 TTY 中运行,从而进入 TUI/REPL 模式。
9
+ - `@xterm/headless`:在 Node 环境中模拟终端(解析 ANSI 控制序列、光标移动、清屏、局部重绘、scrollback 等),**不渲染界面**。
10
+ - 关键:你不再把 PTY 输出当 “stdout 文本流”,而是当作 “终端屏幕刷新指令”。因此必须通过终端仿真器维护 screen buffer,再从 buffer 做匹配/抽取。
11
+
12
+ 推荐加:`@xterm/addon-serialize` 来序列化 framebuffer,用于日志、diff、回归测试。
13
+
14
+ ---
15
+
16
+ ## 2. 技术栈与依赖
17
+
18
+ ### Node 依赖
19
+ - `node-pty`:spawn 交互式 CLI,输入/输出、resize、signal。
20
+ - `@xterm/headless`:headless 终端仿真器。
21
+ - `@xterm/addon-serialize`:将屏幕/scrollback 序列化为字符串(可选但强烈推荐)。
22
+ - (可选)`fast-diff` / `diff-match-patch`:用于文本 diff。
23
+ - (可选)`p-queue`:用于请求串行队列(避免输出交错)。
24
+
25
+ ---
26
+
27
+ ## 3. 架构拆分
28
+
29
+ 建议文件结构:
30
+
31
+ ```
32
+ src/
33
+ pty/
34
+ PtySession.ts
35
+ term/
36
+ HeadlessScreen.ts
37
+ ScreenSnapshot.ts
38
+ expect/
39
+ ExpectEngine.ts
40
+ Matchers.ts
41
+ driver/
42
+ TuiProfile.ts
43
+ TuiDriver.ts
44
+ StateMachine.ts
45
+ profiles/
46
+ claudeCode.profile.ts
47
+ codex.profile.ts
48
+ extract/
49
+ OutputExtractor.ts
50
+ Diff.ts
51
+ index.ts
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 4. 关键模块设计
57
+
58
+ ### 4.1 PtySession(node-pty wrapper)
59
+ 职责:
60
+ - spawn/kill CLI
61
+ - write 输入(按键、文本、粘贴)
62
+ - resize(保持 cols/rows 固定)
63
+ - onData 推送输出(raw chunk)
64
+
65
+ 关键点:
66
+ - 固定 `TERM=xterm-256color`
67
+ - 固定 `LANG/LC_ALL` 以减少文案变化(建议英文)
68
+ - 同一 PTY **强制串行**:一次只跑一个 turn
69
+
70
+ ---
71
+
72
+ ### 4.2 HeadlessScreen(@xterm/headless)
73
+ 职责:
74
+ - 创建 Terminal:`new Terminal({ cols, rows, scrollback })`
75
+ - 接收 PTY chunk:`term.write(chunk)`
76
+ - 提供快照 API:`snapshot()` → `ScreenSnapshot`
77
+
78
+ `ScreenSnapshot` 内容建议:
79
+ - `viewportText`:可见区域(rows 行)
80
+ - `scrollbackText`:全量(scrollback + viewport)
81
+ - `cursor`:光标坐标
82
+ - `hash`:屏幕 hash(用于 idle 判断)
83
+ - `timestamp`
84
+
85
+ 实现 viewportText:
86
+ - 直接读 xterm buffer 每行 cell 拼接
87
+ - 或 serialize addon:`serializeAddon.serialize({ ... })`
88
+
89
+ ---
90
+
91
+ ### 4.3 ExpectEngine(屏幕期望/等待引擎)
92
+ 你需要的不止 `waitFor(regex)`,还包括:
93
+
94
+ - **稳定性等待**:某条件成立并持续 stableMs
95
+ - **Idle 等待**:屏幕 hash N ms 无变化
96
+ - **区域匹配**:仅在底栏/输入框区域搜索(减少误判)
97
+ - **超时 recovery hook**:超时后执行 Esc/Ctrl+C 或重启
98
+
99
+ 接口示例:
100
+ ```ts
101
+ await expect.until({
102
+ name: "READY",
103
+ match: snap => /Ready|New chat|>/i.test(snap.viewportText),
104
+ stableMs: 300,
105
+ timeoutMs: 15000,
106
+ });
107
+
108
+ await expect.untilIdle({
109
+ name: "STREAM_END",
110
+ idleMs: 800,
111
+ timeoutMs: 120000,
112
+ });
113
+ ```
114
+
115
+ ---
116
+
117
+ ### 4.4 TuiProfile(工具差异配置层)
118
+ 不同工具 TUI 行为不同(锚点、快捷键、底栏文案)。必须配置化:
119
+
120
+ ```ts
121
+ export type TuiProfile = {
122
+ name: "claude-code" | "codex";
123
+ command: string;
124
+ args: string[];
125
+ env?: Record<string,string>;
126
+
127
+ anchors: {
128
+ ready: RegExp[]; // 输入框可用/就绪
129
+ busy?: RegExp[]; // 生成中
130
+ error?: RegExp[]; // 断网/鉴权失败
131
+ };
132
+
133
+ keys: {
134
+ submit: string[]; // 如 ["ENTER"] 或 ["CTRL_J"]
135
+ newChat?: string[];
136
+ cancel?: string[]; // 如 ["ESC","ESC"] 或 ["CTRL_C"]
137
+ };
138
+
139
+ extraction: {
140
+ mode: "diff-viewport" | "diff-scrollback";
141
+ stripPatterns: RegExp[];
142
+ stripEcho?: boolean;
143
+ bottomUiLines?: number; // 底栏高度
144
+ };
145
+ };
146
+ ```
147
+
148
+ 你最终只需要维护 2 份 Profile:
149
+ - `claudeCode.profile.ts`
150
+ - `codex.profile.ts`
151
+
152
+ ---
153
+
154
+ ## 5. 状态机(StateMachine)设计
155
+
156
+ 通用状态:
157
+
158
+ 1) **BOOT**
159
+ - spawn PTY
160
+ - 等待 UI 稳定(出现 ready/busy/error 任一)
161
+
162
+ 2) **WAIT_READY**
163
+ - expect ready anchor(稳定 N ms)
164
+
165
+ 3) **PREPARE_TURN(可选)**
166
+ - 新会话/清理上下文(newChat keys)
167
+ - 清弹窗:Esc Esc
168
+
169
+ 4) **TYPE_PROMPT**
170
+ - 输入 prompt(分块粘贴或逐字)
171
+ - 可控速率(避免 TUI 丢字符)
172
+
173
+ 5) **SUBMIT**
174
+ - 发送:ENTER / Ctrl+J / Ctrl+Enter 等
175
+
176
+ 6) **WAIT_STREAM_START**
177
+ - 出现 busy anchor 或 hash 开始快速变化
178
+
179
+ 7) **WAIT_STREAM_END**
180
+ - busy 消失 + idleMs 达标(组合更稳)
181
+
182
+ 8) **CAPTURE**
183
+ - 快照 before/after
184
+ - 输出抽取
185
+
186
+ 9) **DONE / ERROR**
187
+ - 失败进入 recovery:cancel 或重启
188
+
189
+ ---
190
+
191
+ ## 6. 输出抽取策略(核心难点)
192
+
193
+ ### 推荐:diff + 过滤(最通用)
194
+ 流程:
195
+ 1. 发送前保存 `before = snapshot()`
196
+ 2. 结束后保存 `after = snapshot()`
197
+ 3. diff `viewportText` / `scrollbackText`
198
+ 4. 新增部分作为候选 output
199
+ 5. 过滤掉底栏/快捷键提示/状态栏(stripPatterns)
200
+
201
+ ### 进阶:区域抽取
202
+ 多数 TUI:
203
+ - 输出区在上
204
+ - 输入框在下
205
+ - 底部有 help bar
206
+
207
+ 建议:只抽取 viewport 的 **上半/去掉底部 N 行**:
208
+ - `bottomUiLines = 2~5`
209
+
210
+ ---
211
+
212
+ ## 7. 关键鲁棒性细节(必须做)
213
+
214
+ ### 7.1 固定尺寸
215
+ - `pty.spawn(..., { cols: 120, rows: 40 })`
216
+ - `new Terminal({ cols:120, rows:40 })`
217
+ - 必要时同步 resize
218
+
219
+ ### 7.2 输入节流
220
+ 大 prompt 粘贴分块:
221
+ - 每 200~500 chars 一块
222
+ - 每块 sleep 10~30ms
223
+ 逐字输入作为 fallback
224
+
225
+ ### 7.3 Recovery 策略
226
+ 常见:
227
+ - `ESC ESC`:清弹窗
228
+ - `CTRL_C`:中断生成
229
+ - 超时仍无解:kill + restart(回到 BOOT)
230
+
231
+ ### 7.4 串行队列
232
+ 一个 PTY driver 一次只处理一个 ask。
233
+ 否则输出混在同一 screen buffer 无法区分。
234
+
235
+ ### 7.5 录制回放(强烈建议)
236
+ 记录:
237
+ - 每次 snapshot 的 viewportText + timestamp + hash
238
+ - 或 raw PTY chunk(较大)
239
+ 用于 UI 改版后的回归测试、anchor 修正。
240
+
241
+ ---
242
+
243
+ ## 8. 最小可运行骨架(伪代码)
244
+
245
+ ```ts
246
+ const pty = new PtySession(profile.command, profile.args, { cols, rows, env });
247
+ const screen = new HeadlessScreen({ cols, rows, scrollback: 5000 });
248
+
249
+ pty.onData(chunk => screen.write(chunk));
250
+
251
+ const expect = new ExpectEngine(screen);
252
+ const driver = new TuiDriver({ pty, screen, expect, profile });
253
+
254
+ export async function ask(prompt: string) {
255
+ await driver.ensureReady(); // BOOT + WAIT_READY
256
+ const before = screen.snapshot();
257
+
258
+ await driver.prepareTurn(); // 可选 newChat/clear
259
+ await driver.typePrompt(prompt); // 分块输入
260
+ await driver.submit(); // ENTER
261
+
262
+ await driver.waitStreamStart(); // busy/hash变化
263
+ await driver.waitStreamEnd(); // busy消失 + idle
264
+
265
+ const after = screen.snapshot();
266
+ return extractAnswer(before, after, profile.extraction);
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 9. 落地步骤(建议按周推进)
273
+
274
+ ### Step 1:跑通 headless terminal pipeline
275
+ - spawn claude/codex
276
+ - 把 PTY 输出写入 xterm headless
277
+ - 每 100ms 打印 viewportText 到日志
278
+
279
+ ### Step 2:录制真实交互屏幕快照
280
+ - 手动输入一次 prompt
281
+ - 记录 3 个关键阶段:ready / streaming / done
282
+ - 从快照中提取 anchors
283
+
284
+ ### Step 3:写 Profile
285
+ - ready anchor
286
+ - busy anchor(如有)
287
+ - submit/newChat/cancel key sequences
288
+ - stripPatterns
289
+
290
+ ### Step 4:实现 ExpectEngine + 状态机
291
+ - 先实现 ensureReady + ask 一轮
292
+ - 再加入 timeout/recovery
293
+
294
+ ### Step 5:输出抽取与回归
295
+ - diff-scrollback/viewport
296
+ - strip UI
297
+ - 做 30 条 prompt 回归,统计成功率、误判率
298
+
299
+ ---
300
+
301
+ ## 10. Notes:为什么 TUI driver 不一定比一次性命令更“鲁棒”
302
+ - TUI 改版会破坏锚点
303
+ - 不同 terminal 尺寸导致布局变化
304
+ - 彩色/主题影响字符
305
+ - 重绘节奏让 raw chunk 匹配失效
306
+
307
+ 因此建议:
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@love-moon/tui-driver",
3
+ "version": "0.2.0",
4
+ "description": "TUI Driver for interactive CLI tools like Claude Code and Codex",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "build-pty": "npx node-gyp rebuild --directory=node_modules/.pnpm/node-pty@1.1.0/node_modules/node-pty",
10
+ "dev": "tsc --watch",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:integration": "npx tsx test/integration.ts",
14
+ "test:claude": "npx tsx test/integration.ts --claude",
15
+ "lint": "eslint src --ext .ts"
16
+ },
17
+ "dependencies": {
18
+ "node-pty": "^1.0.0",
19
+ "@xterm/headless": "^5.5.0",
20
+ "@xterm/addon-serialize": "^0.13.0",
21
+ "fast-diff": "^1.3.0",
22
+ "p-queue": "^8.0.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.0",
26
+ "tsx": "^4.7.0",
27
+ "typescript": "^5.3.3",
28
+ "vitest": "^1.2.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ {"onlyBuiltDependencies": ["node-pty", "esbuild"]}