ai-cli-log 1.0.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/README.md +23 -0
- package/ai-cli-log-1.0.0.tgz +0 -0
- package/dist/index.js +118 -0
- package/package.json +34 -0
- package/src/index.ts +95 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# ai-cli-log
|
2
|
+
|
3
|
+
Seamlessly log your AI-powered coding conversations. This command-line interface (CLI) tool captures your terminal interactions with AI models like Gemini and Claude, saving entire sessions as clean Markdown documents for easy review and documentation.
|
4
|
+
|
5
|
+
## Features:
|
6
|
+
|
7
|
+
* **Interactive Session Capture:** Acts as a wrapper for other CLI tools, capturing full interactive sessions, including user input and the "rendered" output (what you actually see on the terminal after backspaces, cursor movements, etc.).
|
8
|
+
* **Accurate Logging:** Utilizes `node-pty` for pseudo-terminal emulation and `@xterm/headless` to parse ANSI escape codes, ensuring the captured log accurately reflects the final state of the terminal screen.
|
9
|
+
* **Markdown Output:** Saves recorded sessions as clean Markdown files for easy readability and documentation.
|
10
|
+
* **TypeScript Implementation:** Built with Node.js and TypeScript, leveraging a robust ecosystem for CLI development and type safety.
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
# ai-cli-log
|
15
|
+
|
16
|
+
无缝记录您的 AI 驱动的编码对话。这个命令行界面 (CLI) 工具捕获您与 Gemini 和 Claude 等 AI 模型的终端交互,并将整个会话保存为整洁的 Markdown 文档,以便于回顾和归档。
|
17
|
+
|
18
|
+
## 功能特点:
|
19
|
+
|
20
|
+
* **交互式会话捕获:** 作为其他 CLI 工具的包装器,捕获完整的交互式会话,包括用户输入和“渲染后”的输出(即在退格、光标移动等操作后您在终端上实际看到的内容)。
|
21
|
+
* **精确日志记录:** 利用 `node-pty` 进行伪终端模拟,并使用 `@xterm/headless` 解析 ANSI 转义码,确保捕获的日志准确反映终端屏幕的最终状态。
|
22
|
+
* **Markdown 输出:** 将记录的会话保存为整洁的 Markdown 文件,便于阅读和文档化。
|
23
|
+
* **TypeScript 实现:** 使用 Node.js 和 TypeScript 构建,利用强大的生态系统进行 CLI 开发和类型安全。
|
Binary file
|
package/dist/index.js
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
const pty = __importStar(require("node-pty"));
|
37
|
+
const fs = __importStar(require("fs"));
|
38
|
+
const path = __importStar(require("path"));
|
39
|
+
const headless_1 = require("@xterm/headless");
|
40
|
+
const args = process.argv.slice(2);
|
41
|
+
const command = args[0];
|
42
|
+
const commandArgs = args.slice(1);
|
43
|
+
if (!command) {
|
44
|
+
console.error('Usage: ai-cli-log <command> [args...]');
|
45
|
+
process.exit(1);
|
46
|
+
}
|
47
|
+
const logsDir = path.join(process.cwd(), '.ai-cli-logs');
|
48
|
+
if (!fs.existsSync(logsDir)) {
|
49
|
+
fs.mkdirSync(logsDir);
|
50
|
+
}
|
51
|
+
// Initialize xterm.js in headless mode
|
52
|
+
const xterm = new headless_1.Terminal({
|
53
|
+
rows: process.stdout.rows,
|
54
|
+
cols: process.stdout.columns,
|
55
|
+
scrollback: Infinity, // Set scrollback to Infinity for unlimited buffer
|
56
|
+
allowProposedApi: true,
|
57
|
+
});
|
58
|
+
const term = pty.spawn(command, commandArgs, {
|
59
|
+
name: 'xterm-color',
|
60
|
+
cols: process.stdout.columns,
|
61
|
+
rows: process.stdout.rows,
|
62
|
+
cwd: process.cwd(),
|
63
|
+
env: process.env,
|
64
|
+
});
|
65
|
+
// Pipe pty output to xterm.js and also to stdout
|
66
|
+
term.onData((data) => {
|
67
|
+
process.stdout.write(data);
|
68
|
+
xterm.write(data);
|
69
|
+
});
|
70
|
+
// Pipe stdin to pty
|
71
|
+
process.stdin.on('data', (data) => {
|
72
|
+
term.write(data.toString());
|
73
|
+
});
|
74
|
+
process.stdin.setRawMode(true);
|
75
|
+
process.stdin.resume();
|
76
|
+
term.onExit(({ exitCode, signal }) => {
|
77
|
+
// Add a small delay to ensure xterm.js has processed all output
|
78
|
+
setTimeout(() => {
|
79
|
+
// Extract rendered text from xterm.js buffer
|
80
|
+
let renderedOutput = '';
|
81
|
+
// Iterate over the entire buffer, including scrollback.
|
82
|
+
// The total number of lines is the sum of lines in scrollback (baseY) and visible rows.
|
83
|
+
for (let i = 0; i < xterm.buffer.active.baseY + xterm.rows; i++) {
|
84
|
+
const line = xterm.buffer.active.getLine(i);
|
85
|
+
if (line) {
|
86
|
+
// translateToString(true) gets the line content, and we trim trailing whitespace.
|
87
|
+
const lineText = line.translateToString(true).replace(/\s+$/, '');
|
88
|
+
renderedOutput += lineText + '\n';
|
89
|
+
}
|
90
|
+
}
|
91
|
+
const now = new Date();
|
92
|
+
const year = now.getFullYear();
|
93
|
+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
94
|
+
const day = now.getDate().toString().padStart(2, '0');
|
95
|
+
const hours = now.getHours().toString().padStart(2, '0');
|
96
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
97
|
+
const seconds = now.getSeconds().toString().padStart(2, '0');
|
98
|
+
const prefix = command || 'session';
|
99
|
+
const logFileName = `${prefix}-${year}${month}${day}-${hours}${minutes}${seconds}.md`;
|
100
|
+
const logFilePath = path.join(logsDir, logFileName);
|
101
|
+
fs.writeFile(logFilePath, renderedOutput, (err) => {
|
102
|
+
if (err) {
|
103
|
+
console.error('Error writing log file:', err);
|
104
|
+
}
|
105
|
+
else {
|
106
|
+
console.log(`Session logged to ${path.relative(process.cwd(), logFilePath)}`);
|
107
|
+
}
|
108
|
+
process.exit(exitCode);
|
109
|
+
});
|
110
|
+
}, 500); // 500ms delay
|
111
|
+
});
|
112
|
+
process.on('SIGINT', () => {
|
113
|
+
term.kill('SIGINT');
|
114
|
+
});
|
115
|
+
process.on('resize', () => {
|
116
|
+
term.resize(process.stdout.columns, process.stdout.rows);
|
117
|
+
xterm.resize(process.stdout.columns, process.stdout.rows);
|
118
|
+
});
|
package/package.json
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"name": "ai-cli-log",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Seamlessly log your AI-powered coding conversations. This command-line interface (CLI) tool captures your terminal interactions with AI models like Gemini and Claude, saving entire sessions as clean Markdown documents for easy review and documentation.",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"bin": {
|
7
|
+
"ai-cli-log": "dist/index.js"
|
8
|
+
},
|
9
|
+
"scripts": {
|
10
|
+
"build": "tsc",
|
11
|
+
"start": "node dist/index.js",
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
13
|
+
},
|
14
|
+
"repository": {
|
15
|
+
"type": "git",
|
16
|
+
"url": "git+https://github.com/alingse/ai-cli-log.git"
|
17
|
+
},
|
18
|
+
"keywords": [],
|
19
|
+
"author": "",
|
20
|
+
"license": "ISC",
|
21
|
+
"bugs": {
|
22
|
+
"url": "https://github.com/alingse/ai-cli-log/issues"
|
23
|
+
},
|
24
|
+
"homepage": "https://github.com/alingse/ai-cli-log#readme",
|
25
|
+
"devDependencies": {
|
26
|
+
"@types/node": "^24.0.10",
|
27
|
+
"ts-node": "^10.9.2",
|
28
|
+
"typescript": "^5.8.3"
|
29
|
+
},
|
30
|
+
"dependencies": {
|
31
|
+
"@xterm/headless": "^5.5.0",
|
32
|
+
"node-pty": "^1.0.0"
|
33
|
+
}
|
34
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
import * as pty from 'node-pty';
|
2
|
+
import * as fs from 'fs';
|
3
|
+
import * as path from 'path';
|
4
|
+
import { Terminal } from '@xterm/headless';
|
5
|
+
|
6
|
+
const args = process.argv.slice(2);
|
7
|
+
const command = args[0];
|
8
|
+
const commandArgs = args.slice(1);
|
9
|
+
|
10
|
+
if (!command) {
|
11
|
+
console.error('Usage: ai-cli-log <command> [args...]');
|
12
|
+
process.exit(1);
|
13
|
+
}
|
14
|
+
|
15
|
+
const logsDir = path.join(process.cwd(), '.ai-cli-logs');
|
16
|
+
if (!fs.existsSync(logsDir)) {
|
17
|
+
fs.mkdirSync(logsDir);
|
18
|
+
}
|
19
|
+
|
20
|
+
// Initialize xterm.js in headless mode
|
21
|
+
const xterm = new Terminal({
|
22
|
+
rows: process.stdout.rows,
|
23
|
+
cols: process.stdout.columns,
|
24
|
+
scrollback: Infinity, // Set scrollback to Infinity for unlimited buffer
|
25
|
+
allowProposedApi: true,
|
26
|
+
});
|
27
|
+
|
28
|
+
const term = pty.spawn(command, commandArgs, {
|
29
|
+
name: 'xterm-color',
|
30
|
+
cols: process.stdout.columns,
|
31
|
+
rows: process.stdout.rows,
|
32
|
+
cwd: process.cwd(),
|
33
|
+
env: process.env as { [key: string]: string; },
|
34
|
+
});
|
35
|
+
|
36
|
+
// Pipe pty output to xterm.js and also to stdout
|
37
|
+
term.onData((data) => {
|
38
|
+
process.stdout.write(data);
|
39
|
+
xterm.write(data);
|
40
|
+
});
|
41
|
+
|
42
|
+
// Pipe stdin to pty
|
43
|
+
process.stdin.on('data', (data) => {
|
44
|
+
term.write(data.toString());
|
45
|
+
});
|
46
|
+
|
47
|
+
process.stdin.setRawMode(true);
|
48
|
+
process.stdin.resume();
|
49
|
+
|
50
|
+
term.onExit(({ exitCode, signal }) => {
|
51
|
+
// Add a small delay to ensure xterm.js has processed all output
|
52
|
+
setTimeout(() => {
|
53
|
+
// Extract rendered text from xterm.js buffer
|
54
|
+
let renderedOutput = '';
|
55
|
+
// Iterate over the entire buffer, including scrollback.
|
56
|
+
// The total number of lines is the sum of lines in scrollback (baseY) and visible rows.
|
57
|
+
for (let i = 0; i < xterm.buffer.active.baseY + xterm.rows; i++) {
|
58
|
+
const line = xterm.buffer.active.getLine(i);
|
59
|
+
if (line) {
|
60
|
+
// translateToString(true) gets the line content, and we trim trailing whitespace.
|
61
|
+
const lineText = line.translateToString(true).replace(/\s+$/, '');
|
62
|
+
renderedOutput += lineText + '\n';
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
const now = new Date();
|
67
|
+
const year = now.getFullYear();
|
68
|
+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
69
|
+
const day = now.getDate().toString().padStart(2, '0');
|
70
|
+
const hours = now.getHours().toString().padStart(2, '0');
|
71
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
72
|
+
const seconds = now.getSeconds().toString().padStart(2, '0');
|
73
|
+
const prefix = command || 'session';
|
74
|
+
const logFileName = `${prefix}-${year}${month}${day}-${hours}${minutes}${seconds}.md`;
|
75
|
+
const logFilePath = path.join(logsDir, logFileName);
|
76
|
+
|
77
|
+
fs.writeFile(logFilePath, renderedOutput, (err: NodeJS.ErrnoException | null) => {
|
78
|
+
if (err) {
|
79
|
+
console.error('Error writing log file:', err);
|
80
|
+
} else {
|
81
|
+
console.log(`Session logged to ${path.relative(process.cwd(), logFilePath)}`);
|
82
|
+
}
|
83
|
+
process.exit(exitCode);
|
84
|
+
});
|
85
|
+
}, 500); // 500ms delay
|
86
|
+
});
|
87
|
+
|
88
|
+
process.on('SIGINT', () => {
|
89
|
+
term.kill('SIGINT');
|
90
|
+
});
|
91
|
+
|
92
|
+
process.on('resize', () => {
|
93
|
+
term.resize(process.stdout.columns, process.stdout.rows);
|
94
|
+
xterm.resize(process.stdout.columns, process.stdout.rows);
|
95
|
+
});
|
package/tsconfig.json
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "es2018",
|
4
|
+
"module": "commonjs",
|
5
|
+
"outDir": "./dist",
|
6
|
+
"rootDir": "./src",
|
7
|
+
"strict": true,
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"skipLibCheck": true,
|
10
|
+
"forceConsistentCasingInFileNames": true
|
11
|
+
},
|
12
|
+
"include": ["src/**/*.ts"],
|
13
|
+
"exclude": ["node_modules", "dist"]
|
14
|
+
}
|