@tienne/gestalt 0.5.1 → 0.7.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 (119) hide show
  1. package/README.backup.md +442 -0
  2. package/README.ko.md +487 -0
  3. package/README.md +324 -286
  4. package/dist/package.json +10 -3
  5. package/dist/review-agents/performance-reviewer/AGENT.md +31 -0
  6. package/dist/review-agents/quality-reviewer/AGENT.md +31 -0
  7. package/dist/review-agents/security-reviewer/AGENT.md +32 -0
  8. package/dist/role-agents/architect/AGENT.md +30 -0
  9. package/dist/role-agents/backend-developer/AGENT.md +30 -0
  10. package/dist/role-agents/designer/AGENT.md +30 -0
  11. package/dist/role-agents/devops-engineer/AGENT.md +30 -0
  12. package/dist/role-agents/frontend-developer/AGENT.md +30 -0
  13. package/dist/role-agents/product-planner/AGENT.md +30 -0
  14. package/dist/role-agents/qa-engineer/AGENT.md +30 -0
  15. package/dist/role-agents/researcher/AGENT.md +30 -0
  16. package/dist/role-agents/technical-writer/AGENT.md +212 -0
  17. package/dist/skills/agent/SKILL.md +102 -0
  18. package/dist/skills/execute/SKILL.md +274 -6
  19. package/dist/src/agent/role-agent-registry.d.ts +4 -2
  20. package/dist/src/agent/role-agent-registry.d.ts.map +1 -1
  21. package/dist/src/agent/role-agent-registry.js +12 -3
  22. package/dist/src/agent/role-agent-registry.js.map +1 -1
  23. package/dist/src/cli/commands/interview.d.ts +5 -1
  24. package/dist/src/cli/commands/interview.d.ts.map +1 -1
  25. package/dist/src/cli/commands/interview.js +15 -3
  26. package/dist/src/cli/commands/interview.js.map +1 -1
  27. package/dist/src/cli/index.d.ts.map +1 -1
  28. package/dist/src/cli/index.js +4 -2
  29. package/dist/src/cli/index.js.map +1 -1
  30. package/dist/src/core/config.d.ts +3 -0
  31. package/dist/src/core/config.d.ts.map +1 -1
  32. package/dist/src/core/config.js +4 -0
  33. package/dist/src/core/config.js.map +1 -1
  34. package/dist/src/core/types.d.ts +28 -0
  35. package/dist/src/core/types.d.ts.map +1 -1
  36. package/dist/src/mcp/schemas.d.ts +3 -0
  37. package/dist/src/mcp/schemas.d.ts.map +1 -1
  38. package/dist/src/mcp/schemas.js +2 -0
  39. package/dist/src/mcp/schemas.js.map +1 -1
  40. package/dist/src/mcp/server.d.ts.map +1 -1
  41. package/dist/src/mcp/server.js +12 -1
  42. package/dist/src/mcp/server.js.map +1 -1
  43. package/dist/src/mcp/tools/agent-passthrough.d.ts +7 -0
  44. package/dist/src/mcp/tools/agent-passthrough.d.ts.map +1 -0
  45. package/dist/src/mcp/tools/agent-passthrough.js +49 -0
  46. package/dist/src/mcp/tools/agent-passthrough.js.map +1 -0
  47. package/dist/src/mcp/tools/interview-passthrough.d.ts.map +1 -1
  48. package/dist/src/mcp/tools/interview-passthrough.js +26 -1
  49. package/dist/src/mcp/tools/interview-passthrough.js.map +1 -1
  50. package/dist/src/mcp/tools/interview.d.ts.map +1 -1
  51. package/dist/src/mcp/tools/interview.js +26 -1
  52. package/dist/src/mcp/tools/interview.js.map +1 -1
  53. package/dist/src/recording/agg-converter.d.ts +25 -0
  54. package/dist/src/recording/agg-converter.d.ts.map +1 -0
  55. package/dist/src/recording/agg-converter.js +80 -0
  56. package/dist/src/recording/agg-converter.js.map +1 -0
  57. package/dist/src/recording/agg-installer.d.ts +6 -0
  58. package/dist/src/recording/agg-installer.d.ts.map +1 -0
  59. package/dist/src/recording/agg-installer.js +50 -0
  60. package/dist/src/recording/agg-installer.js.map +1 -0
  61. package/dist/src/recording/asciinema-installer.d.ts +6 -0
  62. package/dist/src/recording/asciinema-installer.d.ts.map +1 -0
  63. package/dist/src/recording/asciinema-installer.js +50 -0
  64. package/dist/src/recording/asciinema-installer.js.map +1 -0
  65. package/dist/src/recording/asciinema-recorder.d.ts +26 -0
  66. package/dist/src/recording/asciinema-recorder.d.ts.map +1 -0
  67. package/dist/src/recording/asciinema-recorder.js +52 -0
  68. package/dist/src/recording/asciinema-recorder.js.map +1 -0
  69. package/dist/src/recording/cast-generator.d.ts +7 -0
  70. package/dist/src/recording/cast-generator.d.ts.map +1 -0
  71. package/dist/src/recording/cast-generator.js +72 -0
  72. package/dist/src/recording/cast-generator.js.map +1 -0
  73. package/dist/src/recording/filename-generator.d.ts +19 -0
  74. package/dist/src/recording/filename-generator.d.ts.map +1 -0
  75. package/dist/src/recording/filename-generator.js +67 -0
  76. package/dist/src/recording/filename-generator.js.map +1 -0
  77. package/dist/src/recording/gif-generator.d.ts +21 -0
  78. package/dist/src/recording/gif-generator.d.ts.map +1 -0
  79. package/dist/src/recording/gif-generator.js +121 -0
  80. package/dist/src/recording/gif-generator.js.map +1 -0
  81. package/dist/src/recording/recording-dir.d.ts +5 -0
  82. package/dist/src/recording/recording-dir.d.ts.map +1 -0
  83. package/dist/src/recording/recording-dir.js +13 -0
  84. package/dist/src/recording/recording-dir.js.map +1 -0
  85. package/dist/src/recording/recording-orchestrator.d.ts +50 -0
  86. package/dist/src/recording/recording-orchestrator.d.ts.map +1 -0
  87. package/dist/src/recording/recording-orchestrator.js +98 -0
  88. package/dist/src/recording/recording-orchestrator.js.map +1 -0
  89. package/dist/src/recording/resume-detector.d.ts +10 -0
  90. package/dist/src/recording/resume-detector.d.ts.map +1 -0
  91. package/dist/src/recording/resume-detector.js +14 -0
  92. package/dist/src/recording/resume-detector.js.map +1 -0
  93. package/dist/src/recording/segment-merger.d.ts +27 -0
  94. package/dist/src/recording/segment-merger.d.ts.map +1 -0
  95. package/dist/src/recording/segment-merger.js +65 -0
  96. package/dist/src/recording/segment-merger.js.map +1 -0
  97. package/dist/src/recording/terminal-recorder.d.ts +31 -0
  98. package/dist/src/recording/terminal-recorder.d.ts.map +1 -0
  99. package/dist/src/recording/terminal-recorder.js +111 -0
  100. package/dist/src/recording/terminal-recorder.js.map +1 -0
  101. package/dist/src/scripts/postinstall.d.ts +2 -0
  102. package/dist/src/scripts/postinstall.d.ts.map +1 -0
  103. package/dist/src/scripts/postinstall.js +27 -0
  104. package/dist/src/scripts/postinstall.js.map +1 -0
  105. package/package.json +10 -3
  106. package/review-agents/performance-reviewer/AGENT.md +31 -0
  107. package/review-agents/quality-reviewer/AGENT.md +31 -0
  108. package/review-agents/security-reviewer/AGENT.md +32 -0
  109. package/role-agents/architect/AGENT.md +30 -0
  110. package/role-agents/backend-developer/AGENT.md +30 -0
  111. package/role-agents/designer/AGENT.md +30 -0
  112. package/role-agents/devops-engineer/AGENT.md +30 -0
  113. package/role-agents/frontend-developer/AGENT.md +30 -0
  114. package/role-agents/product-planner/AGENT.md +30 -0
  115. package/role-agents/qa-engineer/AGENT.md +30 -0
  116. package/role-agents/researcher/AGENT.md +30 -0
  117. package/role-agents/technical-writer/AGENT.md +212 -0
  118. package/skills/agent/SKILL.md +102 -0
  119. package/skills/execute/SKILL.md +274 -6
@@ -0,0 +1,25 @@
1
+ export interface ConvertOptions {
2
+ /** 변환 완료 후 .cast 파일을 삭제할지 여부 (기본값: true) */
3
+ deleteCastAfter?: boolean;
4
+ /** 변환 완료 콜백 (파일 경로 출력 등) */
5
+ onComplete?: (outputPath: string) => void;
6
+ /** 변환 실패 콜백 */
7
+ onError?: (err: Error) => void;
8
+ }
9
+ /**
10
+ * AggConverter: agg 바이너리를 사용해 .cast → GIF 변환을 비동기 백그라운드로 수행한다.
11
+ * convert()는 즉시 return하며, 변환은 백그라운드에서 진행된다.
12
+ */
13
+ export declare class AggConverter {
14
+ /**
15
+ * .cast 파일을 GIF로 변환한다 (백그라운드 비동기).
16
+ * 반환값은 변환 완료를 기다리는 Promise이지만, 호출 측에서 await하지 않아도 된다.
17
+ */
18
+ convertAsync(castPath: string, outputPath: string, options?: ConvertOptions): Promise<string>;
19
+ /**
20
+ * GIF → MP4 변환 (ffmpeg 사용).
21
+ * agg는 gif만 지원하므로 gifPath → mp4Path 변환은 ffmpeg에 위임한다.
22
+ */
23
+ convertGifToMp4Async(gifPath: string, mp4Path: string, options?: ConvertOptions): Promise<string>;
24
+ }
25
+ //# sourceMappingURL=agg-converter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agg-converter.d.ts","sourceRoot":"","sources":["../../../src/recording/agg-converter.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe;IACf,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB;;;OAGG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CjG;;;OAGG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;CAiCtG"}
@@ -0,0 +1,80 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { unlink } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { mkdirSync } from 'node:fs';
5
+ /**
6
+ * AggConverter: agg 바이너리를 사용해 .cast → GIF 변환을 비동기 백그라운드로 수행한다.
7
+ * convert()는 즉시 return하며, 변환은 백그라운드에서 진행된다.
8
+ */
9
+ export class AggConverter {
10
+ /**
11
+ * .cast 파일을 GIF로 변환한다 (백그라운드 비동기).
12
+ * 반환값은 변환 완료를 기다리는 Promise이지만, 호출 측에서 await하지 않아도 된다.
13
+ */
14
+ convertAsync(castPath, outputPath, options = {}) {
15
+ const { deleteCastAfter = true, onComplete, onError } = options;
16
+ mkdirSync(dirname(outputPath), { recursive: true });
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn('agg', [castPath, outputPath], {
19
+ stdio: ['ignore', 'pipe', 'pipe'],
20
+ detached: false,
21
+ });
22
+ child.stderr?.on('data', (data) => {
23
+ const msg = data.toString().trim();
24
+ if (msg)
25
+ process.stderr.write(`[agg] ${msg}\n`);
26
+ });
27
+ child.on('close', async (code) => {
28
+ if (code !== 0) {
29
+ const err = new Error(`agg exited with code ${code}. GIF conversion failed for: ${castPath}`);
30
+ onError?.(err);
31
+ reject(err);
32
+ return;
33
+ }
34
+ if (deleteCastAfter) {
35
+ try {
36
+ await unlink(castPath);
37
+ }
38
+ catch {
39
+ // 삭제 실패는 무시
40
+ }
41
+ }
42
+ onComplete?.(outputPath);
43
+ resolve(outputPath);
44
+ });
45
+ child.on('error', (err) => {
46
+ onError?.(err);
47
+ reject(err);
48
+ });
49
+ });
50
+ }
51
+ /**
52
+ * GIF → MP4 변환 (ffmpeg 사용).
53
+ * agg는 gif만 지원하므로 gifPath → mp4Path 변환은 ffmpeg에 위임한다.
54
+ */
55
+ convertGifToMp4Async(gifPath, mp4Path, options = {}) {
56
+ const { onComplete, onError } = options;
57
+ mkdirSync(dirname(mp4Path), { recursive: true });
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn('ffmpeg', ['-y', '-i', gifPath, '-movflags', 'faststart', '-pix_fmt', 'yuv420p', mp4Path], { stdio: ['ignore', 'pipe', 'pipe'], detached: false });
60
+ child.stderr?.on('data', () => {
61
+ // ffmpeg는 stderr에 진행상황 출력 — 무시
62
+ });
63
+ child.on('close', (code) => {
64
+ if (code !== 0) {
65
+ const err = new Error(`ffmpeg exited with code ${code}. MP4 conversion failed.`);
66
+ onError?.(err);
67
+ reject(err);
68
+ return;
69
+ }
70
+ onComplete?.(mp4Path);
71
+ resolve(mp4Path);
72
+ });
73
+ child.on('error', (err) => {
74
+ onError?.(err);
75
+ reject(err);
76
+ });
77
+ });
78
+ }
79
+ }
80
+ //# sourceMappingURL=agg-converter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agg-converter.js","sourceRoot":"","sources":["../../../src/recording/agg-converter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAWpC;;;GAGG;AACH,MAAM,OAAO,YAAY;IACvB;;;OAGG;IACH,YAAY,CAAC,QAAgB,EAAE,UAAkB,EAAE,UAA0B,EAAE;QAC7E,MAAM,EAAE,eAAe,GAAG,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAEhE,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE;gBACjD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;gBACjC,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;YAEH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;gBACxC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,GAAG;oBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;YAClD,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;gBAC/B,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,wBAAwB,IAAI,gCAAgC,QAAQ,EAAE,CAAC,CAAC;oBAC9F,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;oBACf,MAAM,CAAC,GAAG,CAAC,CAAC;oBACZ,OAAO;gBACT,CAAC;gBAED,IAAI,eAAe,EAAE,CAAC;oBACpB,IAAI,CAAC;wBACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;oBACzB,CAAC;oBAAC,MAAM,CAAC;wBACP,YAAY;oBACd,CAAC;gBACH,CAAC;gBAED,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC;gBACzB,OAAO,CAAC,UAAU,CAAC,CAAC;YACtB,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACxB,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,oBAAoB,CAAC,OAAe,EAAE,OAAe,EAAE,UAA0B,EAAE;QACjF,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAExC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,KAAK,CACjB,QAAQ,EACR,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,EAC/E,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CACvD,CAAC;YAEF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;gBAC5B,+BAA+B;YACjC,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,2BAA2B,IAAI,0BAA0B,CAAC,CAAC;oBACjF,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;oBACf,MAAM,CAAC,GAAG,CAAC,CAAC;oBACZ,OAAO;gBACT,CAAC;gBACD,UAAU,EAAE,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACxB,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export declare class AggInstaller {
2
+ isInstalled(): boolean;
3
+ ensureInstalled(): Promise<void>;
4
+ private hasCommand;
5
+ }
6
+ //# sourceMappingURL=agg-installer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agg-installer.d.ts","sourceRoot":"","sources":["../../../src/recording/agg-installer.ts"],"names":[],"mappings":"AAEA,qBAAa,YAAY;IACvB,WAAW,IAAI,OAAO;IAShB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCtC,OAAO,CAAC,UAAU;CAQnB"}
@@ -0,0 +1,50 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ export class AggInstaller {
3
+ isInstalled() {
4
+ try {
5
+ execSync('which agg', { stdio: 'pipe' });
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ async ensureInstalled() {
13
+ if (this.isInstalled())
14
+ return;
15
+ console.log('📦 agg is not installed. Installing...');
16
+ const hasCargo = this.hasCommand('cargo');
17
+ const hasNpm = this.hasCommand('npm');
18
+ if (hasCargo) {
19
+ console.log(' → cargo install agg');
20
+ const result = spawnSync('cargo', ['install', 'agg'], { stdio: 'inherit' });
21
+ if (result.status !== 0) {
22
+ throw new Error('Failed to install agg via cargo. Please install manually: https://github.com/asciinema/agg');
23
+ }
24
+ }
25
+ else if (hasNpm) {
26
+ console.log(' → npm install -g @asciinema/agg');
27
+ const result = spawnSync('npm', ['install', '-g', '@asciinema/agg'], { stdio: 'inherit' });
28
+ if (result.status !== 0) {
29
+ throw new Error('Failed to install agg via npm. Please install manually: https://github.com/asciinema/agg');
30
+ }
31
+ }
32
+ else {
33
+ throw new Error('Neither cargo nor npm is available. Please install agg manually: https://github.com/asciinema/agg');
34
+ }
35
+ if (!this.isInstalled()) {
36
+ throw new Error('agg installation failed. Please install it manually: https://github.com/asciinema/agg');
37
+ }
38
+ console.log('✅ agg installed successfully.\n');
39
+ }
40
+ hasCommand(cmd) {
41
+ try {
42
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ }
50
+ //# sourceMappingURL=agg-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agg-installer.js","sourceRoot":"","sources":["../../../src/recording/agg-installer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,OAAO,YAAY;IACvB,WAAW;QACT,IAAI,CAAC;YACH,QAAQ,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACzC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO;QAE/B,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;QAEtD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEtC,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACrC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAC5E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CAAC,4FAA4F,CAAC,CAAC;YAChH,CAAC;QACH,CAAC;aAAM,IAAI,MAAM,EAAE,CAAC;YAClB,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAC3F,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CACb,0FAA0F,CAC3F,CAAC;YACJ,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CACb,mGAAmG,CACpG,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;QAC3G,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IACjD,CAAC;IAEO,UAAU,CAAC,GAAW;QAC5B,IAAI,CAAC;YACH,QAAQ,CAAC,SAAS,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export declare class AsciinemaInstaller {
2
+ isInstalled(): boolean;
3
+ ensureInstalled(): Promise<void>;
4
+ private ensureBrewAvailable;
5
+ }
6
+ //# sourceMappingURL=asciinema-installer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciinema-installer.d.ts","sourceRoot":"","sources":["../../../src/recording/asciinema-installer.ts"],"names":[],"mappings":"AAGA,qBAAa,kBAAkB;IAC7B,WAAW,IAAI,OAAO;IAShB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCtC,OAAO,CAAC,mBAAmB;CAS5B"}
@@ -0,0 +1,50 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import { platform } from 'node:os';
3
+ export class AsciinemaInstaller {
4
+ isInstalled() {
5
+ try {
6
+ execSync('which asciinema', { stdio: 'pipe' });
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ async ensureInstalled() {
14
+ if (this.isInstalled())
15
+ return;
16
+ const os = platform();
17
+ console.log('📦 asciinema is not installed. Installing...');
18
+ if (os === 'darwin') {
19
+ this.ensureBrewAvailable();
20
+ console.log(' → brew install asciinema');
21
+ const result = spawnSync('brew', ['install', 'asciinema'], { stdio: 'inherit' });
22
+ if (result.status !== 0) {
23
+ throw new Error('Failed to install asciinema via brew. Please install manually: brew install asciinema');
24
+ }
25
+ }
26
+ else if (os === 'linux') {
27
+ console.log(' → pip3 install asciinema');
28
+ const result = spawnSync('pip3', ['install', 'asciinema'], { stdio: 'inherit' });
29
+ if (result.status !== 0) {
30
+ throw new Error('Failed to install asciinema via pip3. Please install manually: pip3 install asciinema');
31
+ }
32
+ }
33
+ else {
34
+ throw new Error(`Unsupported platform for automatic asciinema installation: ${os}. Please install asciinema manually: https://docs.asciinema.org/manual/cli/installation/`);
35
+ }
36
+ if (!this.isInstalled()) {
37
+ throw new Error('asciinema installation failed. Please install it manually: https://docs.asciinema.org/manual/cli/installation/');
38
+ }
39
+ console.log('✅ asciinema installed successfully.\n');
40
+ }
41
+ ensureBrewAvailable() {
42
+ try {
43
+ execSync('which brew', { stdio: 'pipe' });
44
+ }
45
+ catch {
46
+ throw new Error('Homebrew is not installed. Please install it first: https://brew.sh, then run: brew install asciinema');
47
+ }
48
+ }
49
+ }
50
+ //# sourceMappingURL=asciinema-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciinema-installer.js","sourceRoot":"","sources":["../../../src/recording/asciinema-installer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,MAAM,OAAO,kBAAkB;IAC7B,WAAW;QACT,IAAI,CAAC;YACH,QAAQ,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO;QAE/B,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAE5D,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;YACpB,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACjF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;YAC3G,CAAC;QACH,CAAC;aAAM,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACjF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;YAC3G,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CACb,8DAA8D,EAAE,0FAA0F,CAC3J,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACvD,CAAC;IAEO,mBAAmB;QACzB,IAAI,CAAC;YACH,QAAQ,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,uGAAuG,CACxG,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AsciinemaRecorder: self-respawn 패턴으로 asciinema 녹화를 구현한다.
3
+ *
4
+ * --record 플래그가 있고 GESTALT_RECORDING 환경변수가 없으면,
5
+ * 현재 프로세스를 asciinema rec으로 감싸서 재실행한다.
6
+ * 재실행된 프로세스는 GESTALT_RECORDING=1로 실행되므로 일반 인터뷰 로직을 수행한다.
7
+ */
8
+ export declare class AsciinemaRecorder {
9
+ /** 이미 asciinema로 감싸진 상태인지 확인 */
10
+ static isInsideRecording(): boolean;
11
+ /** 현재 녹화 중인 cast 파일 경로 (GESTALT_CAST_PATH 환경변수) */
12
+ static getCurrentCastPath(): string | undefined;
13
+ /**
14
+ * 임시 cast 파일 경로를 생성한다.
15
+ * 실제 파일명은 인터뷰 완료 후 topic 기반으로 rename된다.
16
+ */
17
+ static createTempCastPath(recordingsDir?: string): string;
18
+ /**
19
+ * asciinema rec으로 현재 프로세스를 재실행한다.
20
+ * 이 함수는 return하지 않는다 (spawnSync가 블로킹).
21
+ *
22
+ * @param castPath - 녹화 결과를 저장할 .cast 파일 경로
23
+ */
24
+ static respawnWithAsciinema(castPath: string): void;
25
+ }
26
+ //# sourceMappingURL=asciinema-recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciinema-recorder.d.ts","sourceRoot":"","sources":["../../../src/recording/asciinema-recorder.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B,gCAAgC;IAChC,MAAM,CAAC,iBAAiB,IAAI,OAAO;IAInC,mDAAmD;IACnD,MAAM,CAAC,kBAAkB,IAAI,MAAM,GAAG,SAAS;IAI/C;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,aAAa,SAAwB,GAAG,MAAM;IAKxE;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAmBpD"}
@@ -0,0 +1,52 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ /**
6
+ * AsciinemaRecorder: self-respawn 패턴으로 asciinema 녹화를 구현한다.
7
+ *
8
+ * --record 플래그가 있고 GESTALT_RECORDING 환경변수가 없으면,
9
+ * 현재 프로세스를 asciinema rec으로 감싸서 재실행한다.
10
+ * 재실행된 프로세스는 GESTALT_RECORDING=1로 실행되므로 일반 인터뷰 로직을 수행한다.
11
+ */
12
+ export class AsciinemaRecorder {
13
+ /** 이미 asciinema로 감싸진 상태인지 확인 */
14
+ static isInsideRecording() {
15
+ return process.env['GESTALT_RECORDING'] === '1';
16
+ }
17
+ /** 현재 녹화 중인 cast 파일 경로 (GESTALT_CAST_PATH 환경변수) */
18
+ static getCurrentCastPath() {
19
+ return process.env['GESTALT_CAST_PATH'];
20
+ }
21
+ /**
22
+ * 임시 cast 파일 경로를 생성한다.
23
+ * 실제 파일명은 인터뷰 완료 후 topic 기반으로 rename된다.
24
+ */
25
+ static createTempCastPath(recordingsDir = '.gestalt/recordings') {
26
+ mkdirSync(recordingsDir, { recursive: true });
27
+ return `${recordingsDir}/tmp-${randomUUID()}.cast`;
28
+ }
29
+ /**
30
+ * asciinema rec으로 현재 프로세스를 재실행한다.
31
+ * 이 함수는 return하지 않는다 (spawnSync가 블로킹).
32
+ *
33
+ * @param castPath - 녹화 결과를 저장할 .cast 파일 경로
34
+ */
35
+ static respawnWithAsciinema(castPath) {
36
+ // 현재 process.argv에서 node 실행파일을 제외한 스크립트 + 인자
37
+ const [, ...scriptAndArgs] = process.argv;
38
+ // --record, -r 플래그 제거 (재실행 시 무한루프 방지)
39
+ const filteredArgs = (scriptAndArgs ?? []).filter((a) => a !== '--record' && a !== '-r');
40
+ mkdirSync(dirname(castPath), { recursive: true });
41
+ const result = spawnSync('asciinema', ['rec', '--overwrite', castPath, '--', 'node', ...filteredArgs], {
42
+ stdio: 'inherit',
43
+ env: {
44
+ ...process.env,
45
+ GESTALT_RECORDING: '1',
46
+ GESTALT_CAST_PATH: castPath,
47
+ },
48
+ });
49
+ process.exit(result.status ?? 0);
50
+ }
51
+ }
52
+ //# sourceMappingURL=asciinema-recorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciinema-recorder.js","sourceRoot":"","sources":["../../../src/recording/asciinema-recorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,OAAO,iBAAiB;IAC5B,gCAAgC;IAChC,MAAM,CAAC,iBAAiB;QACtB,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,KAAK,GAAG,CAAC;IAClD,CAAC;IAED,mDAAmD;IACnD,MAAM,CAAC,kBAAkB;QACvB,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,aAAa,GAAG,qBAAqB;QAC7D,SAAS,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,OAAO,GAAG,aAAa,QAAQ,UAAU,EAAE,OAAO,CAAC;IACrD,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAgB;QAC1C,6CAA6C;QAC7C,MAAM,CAAC,EAAE,GAAG,aAAa,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1C,sCAAsC;QACtC,MAAM,YAAY,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAEzF,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAElD,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,EAAE;YACrG,KAAK,EAAE,SAAS;YAChB,GAAG,EAAE;gBACH,GAAG,OAAO,CAAC,GAAG;gBACd,iBAAiB,EAAE,GAAG;gBACtB,iBAAiB,EAAE,QAAQ;aAC5B;SACF,CAAC,CAAC;QAEH,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IACnC,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import type { InterviewSession } from '../core/types.js';
2
+ export declare function slugify(topic: string): string;
3
+ export declare function getDateString(date?: Date): string;
4
+ export declare class CastGenerator {
5
+ generate(session: InterviewSession, outputPath: string): void;
6
+ }
7
+ //# sourceMappingURL=cast-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cast-generator.d.ts","sourceRoot":"","sources":["../../../src/recording/cast-generator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAYzD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,wBAAgB,aAAa,CAAC,IAAI,OAAa,GAAG,MAAM,CAEvD;AAED,qBAAa,aAAa;IACxB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;CAyD9D"}
@@ -0,0 +1,72 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ // ANSI color codes
4
+ const RESET = '\x1b[0m';
5
+ const BOLD = '\x1b[1m';
6
+ const CYAN = '\x1b[36m';
7
+ const YELLOW = '\x1b[33m';
8
+ const GREEN = '\x1b[32m';
9
+ const DIM = '\x1b[2m';
10
+ export function slugify(topic) {
11
+ return topic
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]+/g, '-')
14
+ .replace(/^-+|-+$/g, '')
15
+ .slice(0, 50) || 'interview';
16
+ }
17
+ export function getDateString(date = new Date()) {
18
+ return date.toISOString().slice(0, 10).replace(/-/g, '');
19
+ }
20
+ export class CastGenerator {
21
+ generate(session, outputPath) {
22
+ const startTs = Math.floor(Date.parse(session.createdAt) / 1000);
23
+ const header = {
24
+ version: 2,
25
+ width: 100,
26
+ height: 40,
27
+ timestamp: startTs,
28
+ title: `Gestalt Interview: ${session.topic}`,
29
+ };
30
+ const events = [];
31
+ let t = 0;
32
+ // Banner
33
+ events.push([t, 'o', `\r\n${BOLD}${CYAN}╔══════════════════════════════════════════════╗${RESET}\r\n`]);
34
+ t += 0.05;
35
+ events.push([t, 'o', `${BOLD}${CYAN}║ 🎯 Gestalt Interview ║${RESET}\r\n`]);
36
+ t += 0.05;
37
+ events.push([t, 'o', `${BOLD}${CYAN}║ ${DIM}${session.topic.slice(0, 44).padEnd(44)}${RESET}${BOLD}${CYAN} ║${RESET}\r\n`]);
38
+ t += 0.05;
39
+ events.push([t, 'o', `${BOLD}${CYAN}╚══════════════════════════════════════════════╝${RESET}\r\n\r\n`]);
40
+ t += 0.5;
41
+ // Q&A rounds
42
+ for (const round of session.rounds) {
43
+ if (!round.userResponse)
44
+ continue;
45
+ // Question
46
+ events.push([t, 'o', `${BOLD}${YELLOW}Q${round.roundNumber} [${round.gestaltFocus}]${RESET}\r\n`]);
47
+ t += 0.1;
48
+ events.push([t, 'o', `${round.question}\r\n\r\n`]);
49
+ t += 1.2;
50
+ // Answer
51
+ events.push([t, 'o', `${BOLD}${GREEN}❯${RESET} `]);
52
+ t += 0.1;
53
+ events.push([t, 'o', `${round.userResponse}\r\n\r\n`]);
54
+ t += 0.8;
55
+ }
56
+ // Footer
57
+ events.push([t, 'o', `${BOLD}${CYAN}✅ Interview completed — ${session.rounds.length} rounds${RESET}\r\n`]);
58
+ t += 0.3;
59
+ if (session.ambiguityScore) {
60
+ events.push([t, 'o', `${DIM}Ambiguity score: ${session.ambiguityScore.overall.toFixed(2)}${RESET}\r\n`]);
61
+ }
62
+ events.push([t + 0.2, 'o', '\r\n']);
63
+ // Write file
64
+ mkdirSync(dirname(outputPath), { recursive: true });
65
+ const lines = [
66
+ JSON.stringify(header),
67
+ ...events.map((e) => JSON.stringify(e)),
68
+ ];
69
+ writeFileSync(outputPath, lines.join('\n') + '\n', 'utf8');
70
+ }
71
+ }
72
+ //# sourceMappingURL=cast-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cast-generator.js","sourceRoot":"","sources":["../../../src/recording/cast-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { LLMAdapter } from '../llm/types.js';
2
+ export interface FilenameGeneratorOptions {
3
+ outputDir?: string;
4
+ }
5
+ export declare class FilenameGenerator {
6
+ private readonly llm;
7
+ private readonly options;
8
+ constructor(llm: LLMAdapter, options?: FilenameGeneratorOptions);
9
+ /**
10
+ * 인터뷰 topic 기반으로 kebab-case 이름을 LLM에게 요청하고
11
+ * YYYYMMDD 날짜 접미사를 붙여 GIF 파일명을 생성한다.
12
+ */
13
+ generate(topic: string, sessionId: string): Promise<string>;
14
+ generateCast(topic: string, sessionId: string, outputDir?: string): Promise<string>;
15
+ private requestSlugFromLLM;
16
+ private fallbackSlug;
17
+ private getDateString;
18
+ }
19
+ //# sourceMappingURL=filename-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename-generator.d.ts","sourceRoot":"","sources":["../../../src/recording/filename-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,GAAG,EAAE,UAAU,EACf,OAAO,GAAE,wBAA6B;IAGzD;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ3D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAQ3E,kBAAkB;IA2BhC,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;CAOtB"}
@@ -0,0 +1,67 @@
1
+ export class FilenameGenerator {
2
+ llm;
3
+ options;
4
+ constructor(llm, options = {}) {
5
+ this.llm = llm;
6
+ this.options = options;
7
+ }
8
+ /**
9
+ * 인터뷰 topic 기반으로 kebab-case 이름을 LLM에게 요청하고
10
+ * YYYYMMDD 날짜 접미사를 붙여 GIF 파일명을 생성한다.
11
+ */
12
+ async generate(topic, sessionId) {
13
+ const slug = await this.requestSlugFromLLM(topic, sessionId);
14
+ const date = this.getDateString();
15
+ const filename = `${slug}-${date}.gif`;
16
+ const dir = this.options.outputDir ?? '.';
17
+ return dir === '.' ? filename : `${dir}/${filename}`;
18
+ }
19
+ async generateCast(topic, sessionId, outputDir) {
20
+ const slug = await this.requestSlugFromLLM(topic, sessionId);
21
+ const date = this.getDateString();
22
+ const filename = `${slug}-${date}.cast`;
23
+ const dir = outputDir ?? this.options.outputDir ?? '.gestalt/recordings';
24
+ return `${dir}/${filename}`;
25
+ }
26
+ async requestSlugFromLLM(topic, sessionId) {
27
+ try {
28
+ const response = await this.llm.chat({
29
+ system: 'You are a file naming assistant. Respond with ONLY a kebab-case slug (2-5 words, lowercase, hyphens only, no spaces, no special chars, no extension).',
30
+ messages: [
31
+ {
32
+ role: 'user',
33
+ content: `Generate a descriptive kebab-case filename for a terminal recording of an interview about: "${topic}"\n\nSession: ${sessionId}\n\nRespond with ONLY the kebab-case slug, nothing else.`,
34
+ },
35
+ ],
36
+ maxTokens: 50,
37
+ temperature: 0.3,
38
+ });
39
+ const slug = response.content
40
+ .trim()
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9-]/g, '-')
43
+ .replace(/-+/g, '-')
44
+ .replace(/^-|-$/g, '');
45
+ return slug || this.fallbackSlug(topic);
46
+ }
47
+ catch {
48
+ return this.fallbackSlug(topic);
49
+ }
50
+ }
51
+ fallbackSlug(topic) {
52
+ return topic
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9\s]/g, '')
55
+ .trim()
56
+ .replace(/\s+/g, '-')
57
+ .slice(0, 40) || 'interview';
58
+ }
59
+ getDateString() {
60
+ const now = new Date();
61
+ const year = now.getFullYear();
62
+ const month = String(now.getMonth() + 1).padStart(2, '0');
63
+ const day = String(now.getDate()).padStart(2, '0');
64
+ return `${year}${month}${day}`;
65
+ }
66
+ }
67
+ //# sourceMappingURL=filename-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename-generator.js","sourceRoot":"","sources":["../../../src/recording/filename-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { TerminalFrame, GifOutput } from '../core/types.js';
2
+ export interface GifGeneratorOptions {
3
+ repeat?: number;
4
+ quality?: number;
5
+ frameDelay?: number;
6
+ }
7
+ export declare class GifGenerator {
8
+ private readonly repeat;
9
+ private readonly quality;
10
+ private readonly frameDelay;
11
+ constructor(options?: GifGeneratorOptions);
12
+ /** .frames NDJSON 파일을 읽어 GIF 파일로 변환 */
13
+ generate(framesPath: string, outputPath: string): Promise<GifOutput>;
14
+ /** TerminalFrame 배열을 직접 받아 GIF 생성 (SegmentMerger에서 병합된 결과 사용) */
15
+ generateFromFrames(frames: TerminalFrame[], outputPath: string): Promise<GifOutput>;
16
+ readFrames(framesPath: string): Promise<TerminalFrame[]>;
17
+ private encodeGif;
18
+ private renderFrame;
19
+ private loadTerminalFont;
20
+ }
21
+ //# sourceMappingURL=gif-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gif-generator.d.ts","sourceRoot":"","sources":["../../../src/recording/gif-generator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAWjE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,GAAE,mBAAwB;IAM7C,uCAAuC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAa1E,iEAAiE;IAC3D,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAOnF,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;YAqBhD,SAAS;YA6CT,WAAW;YAuBX,gBAAgB;CAS/B"}