logifai 0.1.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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/capture.d.ts +3 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +65 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +74 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/normalizer.d.ts +15 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +160 -0
- package/dist/normalizer.js.map +1 -0
- package/dist/redactor.d.ts +4 -0
- package/dist/redactor.d.ts.map +1 -0
- package/dist/redactor.js +78 -0
- package/dist/redactor.js.map +1 -0
- package/dist/session.d.ts +6 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +34 -0
- package/dist/session.js.map +1 -0
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +59 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/skills/logifai/SKILL.md +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tomoya Fujita
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# logifai
|
|
2
|
+
|
|
3
|
+
**Auto-capture development logs for Claude Code — stop copy-pasting terminal output.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/logifai)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
When debugging with Claude Code, you're constantly doing this:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
1. Run your dev server
|
|
15
|
+
2. Hit an error
|
|
16
|
+
3. Scroll through terminal output
|
|
17
|
+
4. Copy the error message
|
|
18
|
+
5. Paste it into Claude Code
|
|
19
|
+
6. Ask "what's this?"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**logifai eliminates steps 3-5.** Your logs are always there — just ask Claude.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. npm run dev 2>&1 | logifai capture
|
|
26
|
+
2. Hit an error
|
|
27
|
+
3. Ask Claude Code "what went wrong?"
|
|
28
|
+
4. Claude automatically searches your logs and answers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Pipe & Capture** — `command 2>&1 | logifai capture` records everything
|
|
34
|
+
- **Smart Normalization** — auto-detects JSON, infers log levels (ERROR/WARN/INFO/DEBUG), groups stack traces
|
|
35
|
+
- **Automatic Redaction** — API keys, tokens, passwords, and connection strings are masked before storage
|
|
36
|
+
- **Claude Code Skill** — Claude searches your logs automatically when you ask about errors
|
|
37
|
+
- **NDJSON Storage** — structured, greppable, `jq`-friendly format
|
|
38
|
+
- **Zero Dependencies** — built on Node.js standard library only
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g logifai
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Capture logs
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run dev 2>&1 | logifai capture
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Output passes through to your terminal as normal — logifai records it in the background.
|
|
55
|
+
|
|
56
|
+
### 3. Install the Claude Code Skill
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cp -r "$(npm root -g)/logifai/skills/logifai" ~/.claude/skills/logifai
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. Ask Claude
|
|
63
|
+
|
|
64
|
+
Just ask Claude Code naturally:
|
|
65
|
+
|
|
66
|
+
- "What errors happened recently?"
|
|
67
|
+
- "Show me the stack trace from the last failure"
|
|
68
|
+
- "What went wrong with the API call?"
|
|
69
|
+
|
|
70
|
+
Claude automatically searches your captured logs and responds with context.
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
stdin ──→ Normalizer ──→ Redactor ──→ NDJSON file
|
|
76
|
+
│ │
|
|
77
|
+
├─ JSON auto-detect └─ ~/.local/state/logifai/logs/
|
|
78
|
+
├─ Level inference ├─ session-*.ndjson
|
|
79
|
+
├─ Stack trace grouping └─ current.ndjson (symlink)
|
|
80
|
+
└─ CLF/Syslog parsing
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Each log line becomes a structured JSON entry:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"timestamp": "2026-02-08T10:30:45.123Z",
|
|
88
|
+
"level": "ERROR",
|
|
89
|
+
"message": "Module not found: @/components/Button",
|
|
90
|
+
"source": "npm-run-dev",
|
|
91
|
+
"project": "/home/user/my-app",
|
|
92
|
+
"session_id": "a1b2c3d4",
|
|
93
|
+
"git_branch": "feature/auth",
|
|
94
|
+
"stack": "Error: Module not found\n at Object.<anonymous> ..."
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The Claude Code Skill (`~/.claude/skills/logifai/SKILL.md`) gives Claude the knowledge to search these files using `grep`, `jq`, and standard tools — no MCP server needed.
|
|
99
|
+
|
|
100
|
+
## CLI Reference
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
logifai capture [options]
|
|
104
|
+
|
|
105
|
+
Options:
|
|
106
|
+
--source <name> Source label for log entries (default: "unknown")
|
|
107
|
+
--project <path> Project path (default: current directory)
|
|
108
|
+
--no-passthrough Don't echo stdin to stdout
|
|
109
|
+
--help Show help
|
|
110
|
+
--version Show version
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Examples
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Basic capture
|
|
117
|
+
npm run dev 2>&1 | logifai capture
|
|
118
|
+
|
|
119
|
+
# Label the source
|
|
120
|
+
npm run build 2>&1 | logifai capture --source build
|
|
121
|
+
|
|
122
|
+
# Capture without terminal echo
|
|
123
|
+
npm test 2>&1 | logifai capture --no-passthrough
|
|
124
|
+
|
|
125
|
+
# Capture any command
|
|
126
|
+
docker compose up 2>&1 | logifai capture --source docker
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Storage
|
|
130
|
+
|
|
131
|
+
Logs are stored following the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir/latest/):
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
~/.local/state/logifai/logs/
|
|
135
|
+
├── session-20260208-103045-a1b2c3d4.ndjson # Session files
|
|
136
|
+
├── session-20260208-140522-e5f6g7h8.ndjson
|
|
137
|
+
└── current.ndjson -> session-...ndjson # Symlink to latest
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Respects `$XDG_STATE_HOME` if set.
|
|
141
|
+
|
|
142
|
+
### Manual Queries
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Recent errors
|
|
146
|
+
grep '"level":"ERROR"' ~/.local/state/logifai/logs/current.ndjson
|
|
147
|
+
|
|
148
|
+
# Errors with context
|
|
149
|
+
grep -B 5 -A 5 "Module not found" ~/.local/state/logifai/logs/current.ndjson
|
|
150
|
+
|
|
151
|
+
# With jq
|
|
152
|
+
jq 'select(.level == "ERROR" and .stack != null)' ~/.local/state/logifai/logs/current.ndjson
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Security
|
|
156
|
+
|
|
157
|
+
### Automatic Redaction
|
|
158
|
+
|
|
159
|
+
Sensitive data is automatically masked before being written to disk:
|
|
160
|
+
|
|
161
|
+
| Pattern | Example |
|
|
162
|
+
|---------|---------|
|
|
163
|
+
| Bearer tokens | `Bearer [REDACTED]` |
|
|
164
|
+
| GitHub PATs | `[REDACTED]` |
|
|
165
|
+
| OpenAI/Anthropic API keys | `[REDACTED]` |
|
|
166
|
+
| AWS Access Key IDs | `[REDACTED]` |
|
|
167
|
+
| Database connection strings | `postgres://[REDACTED]:[REDACTED]@...` |
|
|
168
|
+
| JWT tokens | `[REDACTED]` |
|
|
169
|
+
| Generic secrets (`api_key=...`, `token=...`) | `api_key=[REDACTED]` |
|
|
170
|
+
| Private key blocks | `[REDACTED]` |
|
|
171
|
+
|
|
172
|
+
### File Permissions
|
|
173
|
+
|
|
174
|
+
- Log directory: `700` (owner-only access)
|
|
175
|
+
- Log files: `600` (owner read/write only)
|
|
176
|
+
|
|
177
|
+
### Local Only
|
|
178
|
+
|
|
179
|
+
All data stays on your machine. No external services, no telemetry, no network calls.
|
|
180
|
+
|
|
181
|
+
## Roadmap
|
|
182
|
+
|
|
183
|
+
| Phase | Status | Description |
|
|
184
|
+
|-------|--------|-------------|
|
|
185
|
+
| **Phase 1** | Done | Pipe capture, NDJSON storage, normalizer, redactor, Claude Code Skill |
|
|
186
|
+
| **Phase 2** | Planned | `logifai exec` — child process mode with TTY propagation and signal forwarding |
|
|
187
|
+
| **Phase 3** | Planned | SQLite FTS5 index, `.logifai.toml` config file, `logifai start` |
|
|
188
|
+
| **Phase 4** | Planned | MCP server, semantic search, anomaly detection |
|
|
189
|
+
|
|
190
|
+
See [doc.md](doc.md) for the full technical specification.
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
Contributions are welcome! Please:
|
|
195
|
+
|
|
196
|
+
1. Fork the repository
|
|
197
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
198
|
+
3. Make your changes
|
|
199
|
+
4. Run tests: `npm test`
|
|
200
|
+
5. Commit and push
|
|
201
|
+
6. Open a Pull Request
|
|
202
|
+
|
|
203
|
+
### Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/user/logifai.git
|
|
207
|
+
cd logifai
|
|
208
|
+
npm install
|
|
209
|
+
npm run build
|
|
210
|
+
npm test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAY,MAAM,YAAY,CAAC;AAM3D,wBAAsB,OAAO,CAC3B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAmEf"}
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createSession } from "./session.js";
|
|
4
|
+
import { normalizeLine, isStackTraceLine, stripAnsi } from "./normalizer.js";
|
|
5
|
+
import { redactLogEntry } from "./redactor.js";
|
|
6
|
+
import { ensureLogsDir, NdjsonWriter, updateCurrentSymlink } from "./storage.js";
|
|
7
|
+
export async function capture(input, options) {
|
|
8
|
+
const session = await createSession();
|
|
9
|
+
const dir = await ensureLogsDir();
|
|
10
|
+
const filePath = join(dir, session.filename);
|
|
11
|
+
const writer = new NdjsonWriter(filePath);
|
|
12
|
+
await updateCurrentSymlink(dir, session.filename);
|
|
13
|
+
// Ignore SIGPIPE (e.g. downstream consumer closes pipe)
|
|
14
|
+
process.on("SIGPIPE", () => { });
|
|
15
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
16
|
+
let pendingEntry = null;
|
|
17
|
+
let stackLines = [];
|
|
18
|
+
function flushPending() {
|
|
19
|
+
if (pendingEntry) {
|
|
20
|
+
if (stackLines.length > 0) {
|
|
21
|
+
pendingEntry.stack = stackLines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
writer.write(redactLogEntry(pendingEntry));
|
|
24
|
+
pendingEntry = null;
|
|
25
|
+
stackLines = [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for await (const rawLine of rl) {
|
|
29
|
+
// Passthrough: echo to stdout
|
|
30
|
+
if (options.passthrough) {
|
|
31
|
+
process.stdout.write(rawLine + "\n");
|
|
32
|
+
}
|
|
33
|
+
// Skip empty lines
|
|
34
|
+
if (rawLine.trim() === "")
|
|
35
|
+
continue;
|
|
36
|
+
const stripped = stripAnsi(rawLine);
|
|
37
|
+
// Check if this is a stack trace line
|
|
38
|
+
if (isStackTraceLine(stripped)) {
|
|
39
|
+
if (pendingEntry) {
|
|
40
|
+
stackLines.push(stripped);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Orphaned stack trace line — emit as its own entry
|
|
44
|
+
const entry = normalizeLine(rawLine, session, options.source, options.project);
|
|
45
|
+
writer.write(redactLogEntry(entry));
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Not a stack trace line — flush any pending entry first
|
|
50
|
+
flushPending();
|
|
51
|
+
// Normalize new line
|
|
52
|
+
const entry = normalizeLine(rawLine, session, options.source, options.project);
|
|
53
|
+
// If ERROR level, defer writing to accumulate stack traces
|
|
54
|
+
if (entry.level === "ERROR") {
|
|
55
|
+
pendingEntry = entry;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
writer.write(redactLogEntry(entry));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Flush remaining pending entry
|
|
62
|
+
flushPending();
|
|
63
|
+
await writer.close();
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { capture } from "./capture.js";
|
|
3
|
+
const VERSION = "0.1.0";
|
|
4
|
+
const HELP = `logifai - Auto-capture development command output
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
command 2>&1 | logifai capture [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
capture Read stdin and save as NDJSON log
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--source <name> Source label (default: "unknown")
|
|
14
|
+
--project <path> Project path (default: cwd)
|
|
15
|
+
--no-passthrough Don't echo stdin to stdout
|
|
16
|
+
--help Show this help
|
|
17
|
+
--version Show version
|
|
18
|
+
`;
|
|
19
|
+
function parseArgs(args) {
|
|
20
|
+
let command = null;
|
|
21
|
+
let source = "unknown";
|
|
22
|
+
let project = process.cwd();
|
|
23
|
+
let passthrough = true;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const arg = args[i];
|
|
26
|
+
if (arg === "capture") {
|
|
27
|
+
command = "capture";
|
|
28
|
+
}
|
|
29
|
+
else if (arg === "--source" && i + 1 < args.length) {
|
|
30
|
+
source = args[++i];
|
|
31
|
+
}
|
|
32
|
+
else if (arg === "--project" && i + 1 < args.length) {
|
|
33
|
+
project = args[++i];
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--no-passthrough") {
|
|
36
|
+
passthrough = false;
|
|
37
|
+
}
|
|
38
|
+
else if (arg === "--help" || arg === "-h") {
|
|
39
|
+
process.stdout.write(HELP);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
else if (arg === "--version" || arg === "-v") {
|
|
43
|
+
process.stdout.write(VERSION + "\n");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { command, source, project, passthrough };
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
const parsed = parseArgs(args);
|
|
52
|
+
if (!parsed.command) {
|
|
53
|
+
process.stderr.write("Error: No command specified. Use 'logifai capture'.\n");
|
|
54
|
+
process.stderr.write("Run 'logifai --help' for usage.\n");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
if (parsed.command === "capture") {
|
|
58
|
+
if (process.stdin.isTTY) {
|
|
59
|
+
process.stderr.write("Error: No piped input detected.\n");
|
|
60
|
+
process.stderr.write("Usage: command 2>&1 | logifai capture\n");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
await capture(process.stdin, {
|
|
64
|
+
source: parsed.source,
|
|
65
|
+
project: parsed.project,
|
|
66
|
+
passthrough: parsed.passthrough,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
main().catch((err) => {
|
|
71
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { LogEntry, LogLevel, SessionInfo, CaptureOptions } from "./types.js";
|
|
2
|
+
export { capture } from "./capture.js";
|
|
3
|
+
export { createSession, generateSessionId, getGitBranch } from "./session.js";
|
|
4
|
+
export { normalizeLine, detectLevel, isStackTraceLine, stripAnsi } from "./normalizer.js";
|
|
5
|
+
export { redact, redactLogEntry } from "./redactor.js";
|
|
6
|
+
export { logsDir, ensureLogsDir, NdjsonWriter, updateCurrentSymlink } from "./storage.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { capture } from "./capture.js";
|
|
2
|
+
export { createSession, generateSessionId, getGitBranch } from "./session.js";
|
|
3
|
+
export { normalizeLine, detectLevel, isStackTraceLine, stripAnsi } from "./normalizer.js";
|
|
4
|
+
export { redact, redactLogEntry } from "./redactor.js";
|
|
5
|
+
export { logsDir, ensureLogsDir, NdjsonWriter, updateCurrentSymlink } from "./storage.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LogEntry, LogLevel, SessionInfo } from "./types.js";
|
|
2
|
+
export declare function stripAnsi(text: string): string;
|
|
3
|
+
export declare function detectLevel(line: string): LogLevel;
|
|
4
|
+
export declare function isStackTraceLine(line: string): boolean;
|
|
5
|
+
export declare function tryParseJson(line: string): Record<string, unknown> | null;
|
|
6
|
+
interface ParsedFormat {
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
level?: LogLevel;
|
|
9
|
+
message?: string;
|
|
10
|
+
extra?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseCommonFormats(line: string): ParsedFormat | null;
|
|
13
|
+
export declare function normalizeLine(line: string, session: SessionInfo, source: string, project: string): LogEntry;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../src/normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAKlE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAMlD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAatD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYzE;AAcD,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA+CpE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,QAAQ,CA2DV"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// eslint-disable-next-line no-control-regex
|
|
2
|
+
const ANSI_RE = /[\u001b\u009b]\[[0-9;]*[a-zA-Z]/g;
|
|
3
|
+
export function stripAnsi(text) {
|
|
4
|
+
return text.replace(ANSI_RE, "");
|
|
5
|
+
}
|
|
6
|
+
export function detectLevel(line) {
|
|
7
|
+
const stripped = stripAnsi(line);
|
|
8
|
+
if (/error|exception|fatal|ERR!|\u2717|\u274c/i.test(stripped))
|
|
9
|
+
return "ERROR";
|
|
10
|
+
if (/\b(warn|warning)\b|WRN|\u26a0/i.test(stripped))
|
|
11
|
+
return "WARN";
|
|
12
|
+
if (/\b(debug)\b|DBG|\ud83d\udd0d/i.test(stripped))
|
|
13
|
+
return "DEBUG";
|
|
14
|
+
return "INFO";
|
|
15
|
+
}
|
|
16
|
+
export function isStackTraceLine(line) {
|
|
17
|
+
const stripped = stripAnsi(line).trimStart();
|
|
18
|
+
// V8 / Node.js: " at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1077:15)"
|
|
19
|
+
if (/^at\s+.+/.test(stripped))
|
|
20
|
+
return true;
|
|
21
|
+
// Python: " File "/app/main.py", line 10, in <module>"
|
|
22
|
+
if (/^File\s+"[^"]+",\s+line\s+\d+/.test(stripped))
|
|
23
|
+
return true;
|
|
24
|
+
// Java: " at com.example.Main.method(Main.java:42)"
|
|
25
|
+
if (/^at\s+[\w$.]+\([\w.]+:\d+\)/.test(stripped))
|
|
26
|
+
return true;
|
|
27
|
+
// Go: " /app/main.go:42 +0x1a"
|
|
28
|
+
if (/^\/.+\.go:\d+/.test(stripped))
|
|
29
|
+
return true;
|
|
30
|
+
// Rust: " 0: rust_begin_unwind"
|
|
31
|
+
if (/^\d+:\s+\w+/.test(stripped))
|
|
32
|
+
return false; // too generic, skip
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
export function tryParseJson(line) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed.startsWith("{"))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(trimmed);
|
|
41
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not valid JSON
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Apache/Nginx Combined Log Format
|
|
51
|
+
const CLF_RE = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+)/;
|
|
52
|
+
// Syslog format
|
|
53
|
+
const SYSLOG_RE = /^(\w+\s+\d+\s+\d+:\d+:\d+) (\S+) ([^:[\]]+?)(?:\[(\d+)\])?: (.+)$/;
|
|
54
|
+
// ISO timestamp prefix: "2026-02-08T10:30:45.123Z some message"
|
|
55
|
+
const ISO_TS_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2}))\s+(.+)$/;
|
|
56
|
+
export function parseCommonFormats(line) {
|
|
57
|
+
const stripped = stripAnsi(line);
|
|
58
|
+
// ISO timestamp prefix
|
|
59
|
+
const isoMatch = stripped.match(ISO_TS_RE);
|
|
60
|
+
if (isoMatch) {
|
|
61
|
+
return {
|
|
62
|
+
timestamp: isoMatch[1],
|
|
63
|
+
message: isoMatch[2],
|
|
64
|
+
level: detectLevel(isoMatch[2]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Apache/Nginx CLF
|
|
68
|
+
const clfMatch = stripped.match(CLF_RE);
|
|
69
|
+
if (clfMatch) {
|
|
70
|
+
const status = parseInt(clfMatch[6], 10);
|
|
71
|
+
let level = "INFO";
|
|
72
|
+
if (status >= 500)
|
|
73
|
+
level = "ERROR";
|
|
74
|
+
else if (status >= 400)
|
|
75
|
+
level = "WARN";
|
|
76
|
+
return {
|
|
77
|
+
message: `${clfMatch[3]} ${clfMatch[4]} ${clfMatch[5]} ${clfMatch[6]}`,
|
|
78
|
+
level,
|
|
79
|
+
extra: {
|
|
80
|
+
ip: clfMatch[1],
|
|
81
|
+
method: clfMatch[3],
|
|
82
|
+
path: clfMatch[4],
|
|
83
|
+
status,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Syslog
|
|
88
|
+
const syslogMatch = stripped.match(SYSLOG_RE);
|
|
89
|
+
if (syslogMatch) {
|
|
90
|
+
return {
|
|
91
|
+
message: syslogMatch[5],
|
|
92
|
+
level: detectLevel(syslogMatch[5]),
|
|
93
|
+
extra: {
|
|
94
|
+
hostname: syslogMatch[2],
|
|
95
|
+
process: syslogMatch[3],
|
|
96
|
+
syslog_pid: syslogMatch[4] ? parseInt(syslogMatch[4], 10) : undefined,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
export function normalizeLine(line, session, source, project) {
|
|
103
|
+
const stripped = stripAnsi(line);
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
// Try JSON first
|
|
106
|
+
const json = tryParseJson(stripped);
|
|
107
|
+
if (json) {
|
|
108
|
+
const level = typeof json.level === "string" && isValidLevel(json.level)
|
|
109
|
+
? json.level.toUpperCase()
|
|
110
|
+
: detectLevel(stripped);
|
|
111
|
+
return {
|
|
112
|
+
timestamp: typeof json.timestamp === "string" ? json.timestamp : now,
|
|
113
|
+
level,
|
|
114
|
+
message: typeof json.message === "string" ? json.message : stripped,
|
|
115
|
+
source,
|
|
116
|
+
project,
|
|
117
|
+
session_id: session.id,
|
|
118
|
+
git_branch: session.gitBranch,
|
|
119
|
+
pid: process.pid,
|
|
120
|
+
raw: false,
|
|
121
|
+
stack: null,
|
|
122
|
+
_original: json,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Try common formats
|
|
126
|
+
const parsed = parseCommonFormats(stripped);
|
|
127
|
+
if (parsed) {
|
|
128
|
+
return {
|
|
129
|
+
timestamp: parsed.timestamp ?? now,
|
|
130
|
+
level: parsed.level ?? detectLevel(stripped),
|
|
131
|
+
message: parsed.message ?? stripped,
|
|
132
|
+
source,
|
|
133
|
+
project,
|
|
134
|
+
session_id: session.id,
|
|
135
|
+
git_branch: session.gitBranch,
|
|
136
|
+
pid: process.pid,
|
|
137
|
+
raw: false,
|
|
138
|
+
stack: null,
|
|
139
|
+
_original: parsed.extra ?? null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Raw unstructured log
|
|
143
|
+
return {
|
|
144
|
+
timestamp: now,
|
|
145
|
+
level: detectLevel(stripped),
|
|
146
|
+
message: stripped,
|
|
147
|
+
source,
|
|
148
|
+
project,
|
|
149
|
+
session_id: session.id,
|
|
150
|
+
git_branch: session.gitBranch,
|
|
151
|
+
pid: process.pid,
|
|
152
|
+
raw: true,
|
|
153
|
+
stack: null,
|
|
154
|
+
_original: null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function isValidLevel(s) {
|
|
158
|
+
return ["error", "warn", "info", "debug"].includes(s.toLowerCase());
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.js","sourceRoot":"","sources":["../src/normalizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactor.d.ts","sourceRoot":"","sources":["../src/redactor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA4C3C,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAY3C;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAOxD"}
|
package/dist/redactor.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const REDACTED = "[REDACTED]";
|
|
2
|
+
const SENSITIVE_PATTERNS = [
|
|
3
|
+
// Bearer tokens
|
|
4
|
+
{ pattern: /Bearer\s+[A-Za-z0-9\-_.]+/gi, replacement: `Bearer ${REDACTED}` },
|
|
5
|
+
// GitHub PAT (classic & fine-grained)
|
|
6
|
+
{ pattern: /gh[ps]_[A-Za-z0-9]{36,}/g, replacement: REDACTED },
|
|
7
|
+
// OpenAI API Key
|
|
8
|
+
{ pattern: /sk-[A-Za-z0-9]{20,}/g, replacement: REDACTED },
|
|
9
|
+
// Anthropic API Key
|
|
10
|
+
{ pattern: /sk-ant-[A-Za-z0-9\-_]{20,}/g, replacement: REDACTED },
|
|
11
|
+
// AWS Access Key ID
|
|
12
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, replacement: REDACTED },
|
|
13
|
+
// Database connection strings (postgres, mysql, mongodb)
|
|
14
|
+
{
|
|
15
|
+
pattern: /(postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/gi,
|
|
16
|
+
replacement: (match) => {
|
|
17
|
+
const proto = match.split("://")[0];
|
|
18
|
+
return `${proto}://${REDACTED}:${REDACTED}@`;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// JWT tokens
|
|
22
|
+
{
|
|
23
|
+
pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+/g,
|
|
24
|
+
replacement: REDACTED,
|
|
25
|
+
},
|
|
26
|
+
// Generic API key patterns (key=..., api_key=..., apikey=..., token=...)
|
|
27
|
+
{
|
|
28
|
+
pattern: /(api[_-]?key|token|secret|password|credential)[\s]*[=:]\s*["']?[A-Za-z0-9\-_.]{16,}["']?/gi,
|
|
29
|
+
replacement: (match) => {
|
|
30
|
+
const sep = match.includes("=") ? "=" : ":";
|
|
31
|
+
const key = match.split(/[=:]/)[0];
|
|
32
|
+
return `${key}${sep}${REDACTED}`;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// Private key blocks
|
|
36
|
+
{
|
|
37
|
+
pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
38
|
+
replacement: REDACTED,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
export function redact(text) {
|
|
42
|
+
let result = text;
|
|
43
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
44
|
+
// Reset lastIndex for global regexes
|
|
45
|
+
pattern.lastIndex = 0;
|
|
46
|
+
if (typeof replacement === "string") {
|
|
47
|
+
result = result.replace(pattern, replacement);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result = result.replace(pattern, replacement);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export function redactLogEntry(entry) {
|
|
56
|
+
return {
|
|
57
|
+
...entry,
|
|
58
|
+
message: redact(entry.message),
|
|
59
|
+
stack: entry.stack ? redact(entry.stack) : null,
|
|
60
|
+
_original: entry._original ? redactObject(entry._original) : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function redactObject(obj) {
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
result[key] = redact(value);
|
|
68
|
+
}
|
|
69
|
+
else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
70
|
+
result[key] = redactObject(value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=redactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactor.js","sourceRoot":"","sources":["../src/redactor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SessionInfo } from "./types.js";
|
|
2
|
+
export declare function generateSessionId(): string;
|
|
3
|
+
export declare function getGitBranch(): Promise<string | null>;
|
|
4
|
+
export declare function formatSessionFilename(date: Date, id: string): string;
|
|
5
|
+
export declare function createSession(): Promise<SessionInfo>;
|
|
6
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWrD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,CAM1D"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
export function generateSessionId() {
|
|
4
|
+
return randomUUID().slice(0, 8);
|
|
5
|
+
}
|
|
6
|
+
export function getGitBranch() {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], (err, stdout) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
resolve(null);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const branch = stdout.trim();
|
|
14
|
+
resolve(branch || null);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function formatSessionFilename(date, id) {
|
|
19
|
+
const y = date.getFullYear();
|
|
20
|
+
const mo = String(date.getMonth() + 1).padStart(2, "0");
|
|
21
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
22
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
23
|
+
const mi = String(date.getMinutes()).padStart(2, "0");
|
|
24
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
25
|
+
return `session-${y}${mo}${d}-${h}${mi}${s}-${id}.ndjson`;
|
|
26
|
+
}
|
|
27
|
+
export async function createSession() {
|
|
28
|
+
const id = generateSessionId();
|
|
29
|
+
const startedAt = new Date();
|
|
30
|
+
const gitBranch = await getGitBranch();
|
|
31
|
+
const filename = formatSessionFilename(startedAt, id);
|
|
32
|
+
return { id, startedAt, filename, gitBranch };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACrE,IAAI,GAAG,EAAE,CAAC;gBACR,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO;YACT,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC7B,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAU,EAAE,EAAU;IAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAC7B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACnD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACrD,OAAO,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACtD,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LogEntry } from "./types.js";
|
|
2
|
+
export declare function logsDir(): string;
|
|
3
|
+
export declare function ensureLogsDir(): Promise<string>;
|
|
4
|
+
export declare class NdjsonWriter {
|
|
5
|
+
private stream;
|
|
6
|
+
readonly filePath: string;
|
|
7
|
+
constructor(filePath: string);
|
|
8
|
+
write(entry: LogEntry): void;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function updateCurrentSymlink(dir: string, filename: string): Promise<void>;
|
|
12
|
+
export declare function fileExists(path: string): Promise<boolean>;
|
|
13
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAgB,OAAO,IAAI,MAAM,CAGhC;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAIrD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAc;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM;IAQ5B,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQvB;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/D"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdir, symlink, unlink, stat } from "node:fs/promises";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
export function logsDir() {
|
|
6
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
7
|
+
return join(xdgState, "logifai", "logs");
|
|
8
|
+
}
|
|
9
|
+
export async function ensureLogsDir() {
|
|
10
|
+
const dir = logsDir();
|
|
11
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
export class NdjsonWriter {
|
|
15
|
+
stream;
|
|
16
|
+
filePath;
|
|
17
|
+
constructor(filePath) {
|
|
18
|
+
this.filePath = filePath;
|
|
19
|
+
this.stream = createWriteStream(filePath, {
|
|
20
|
+
flags: "a",
|
|
21
|
+
mode: 0o600,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
write(entry) {
|
|
25
|
+
this.stream.write(JSON.stringify(entry) + "\n");
|
|
26
|
+
}
|
|
27
|
+
close() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
this.stream.end((err) => {
|
|
30
|
+
if (err)
|
|
31
|
+
reject(err);
|
|
32
|
+
else
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function updateCurrentSymlink(dir, filename) {
|
|
39
|
+
const linkPath = join(dir, "current.ndjson");
|
|
40
|
+
try {
|
|
41
|
+
await unlink(linkPath);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
// Ignore if symlink doesn't exist
|
|
45
|
+
if (err.code !== "ENOENT")
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
await symlink(filename, linkPath);
|
|
49
|
+
}
|
|
50
|
+
export async function fileExists(path) {
|
|
51
|
+
try {
|
|
52
|
+
await stat(path);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,MAAM,UAAU,OAAO;IACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,YAAY;IACf,MAAM,CAAc;IACnB,QAAQ,CAAS;IAE1B,YAAY,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,iBAAiB,CAAC,QAAQ,EAAE;YACxC,KAAK,EAAE,GAAG;YACV,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAe;QACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,KAAK;QACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE;gBAC9B,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAChB,OAAO,EAAE,CAAC;YACjB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAW,EACX,QAAgB;IAEhB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kCAAkC;QAClC,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IAClE,CAAC;IACD,MAAM,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type LogLevel = "ERROR" | "WARN" | "INFO" | "DEBUG";
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
level: LogLevel;
|
|
5
|
+
message: string;
|
|
6
|
+
source: string;
|
|
7
|
+
project: string;
|
|
8
|
+
session_id: string;
|
|
9
|
+
git_branch: string | null;
|
|
10
|
+
pid: number;
|
|
11
|
+
raw: boolean;
|
|
12
|
+
stack: string | null;
|
|
13
|
+
_original: Record<string, unknown> | null;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
startedAt: Date;
|
|
18
|
+
filename: string;
|
|
19
|
+
gitBranch: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface CaptureOptions {
|
|
22
|
+
source: string;
|
|
23
|
+
project: string;
|
|
24
|
+
passthrough: boolean;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "logifai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auto-capture development command output and search/analyze with Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"logifai": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"author": "Tomoya Fujita",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/tomoyaf/logifai.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/tomoyaf/logifai#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/tomoyaf/logifai/issues"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"!dist/__tests__",
|
|
23
|
+
"skills",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"test": "node --test dist/__tests__/*.test.js",
|
|
30
|
+
"pretest": "tsc",
|
|
31
|
+
"prepublishOnly": "npm run build && npm test"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"logging",
|
|
38
|
+
"capture",
|
|
39
|
+
"cli",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"developer-tools"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"typescript": "^5.7.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logifai-logs
|
|
3
|
+
description: Search and analyze development logs captured by logifai. Use when investigating errors, debugging runtime issues, or reviewing recent application activity. Automatically triggered when user mentions "logs", "errors", "what went wrong", or references recent failures.
|
|
4
|
+
allowed-tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# logifai Development Logs
|
|
8
|
+
|
|
9
|
+
logifai automatically captures output from development commands (e.g. `npm run dev 2>&1 | logifai capture`) and saves them as NDJSON files.
|
|
10
|
+
|
|
11
|
+
## Log Location
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
~/.local/state/logifai/logs/
|
|
15
|
+
├── session-YYYYMMDD-HHmmss-{id}.ndjson # Session files
|
|
16
|
+
└── current.ndjson -> session-...ndjson # Symlink to latest session
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## NDJSON Schema
|
|
20
|
+
|
|
21
|
+
Each line is a JSON object:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"timestamp": "2026-02-08T10:30:45.123Z",
|
|
26
|
+
"level": "ERROR | WARN | INFO | DEBUG",
|
|
27
|
+
"message": "Log message text",
|
|
28
|
+
"source": "npm-run-dev",
|
|
29
|
+
"project": "/home/user/my-app",
|
|
30
|
+
"session_id": "a1b2c3d4",
|
|
31
|
+
"git_branch": "feature/auth",
|
|
32
|
+
"pid": 12345,
|
|
33
|
+
"raw": true,
|
|
34
|
+
"stack": "Error stack trace if detected",
|
|
35
|
+
"_original": {}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Search Commands
|
|
40
|
+
|
|
41
|
+
### List recent sessions
|
|
42
|
+
```bash
|
|
43
|
+
ls -lt ~/.local/state/logifai/logs/session-*.ndjson | head -10
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Search current session for errors
|
|
47
|
+
```bash
|
|
48
|
+
grep '"level":"ERROR"' ~/.local/state/logifai/logs/current.ndjson
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Search all sessions for a keyword
|
|
52
|
+
```bash
|
|
53
|
+
grep -l "keyword" ~/.local/state/logifai/logs/*.ndjson
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Get context around an error
|
|
57
|
+
```bash
|
|
58
|
+
grep -B 5 -A 5 "Module not found" ~/.local/state/logifai/logs/current.ndjson
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Find errors with stack traces
|
|
62
|
+
```bash
|
|
63
|
+
grep '"stack"' ~/.local/state/logifai/logs/current.ndjson | grep -v '"stack":null'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Advanced Queries (with jq)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Last 50 ERROR entries
|
|
70
|
+
tail -n 500 ~/.local/state/logifai/logs/current.ndjson | jq 'select(.level == "ERROR")' | tail -50
|
|
71
|
+
|
|
72
|
+
# Errors after a specific time
|
|
73
|
+
jq 'select(.level == "ERROR" and .timestamp >= "2026-02-08T10:00:00")' ~/.local/state/logifai/logs/current.ndjson
|
|
74
|
+
|
|
75
|
+
# Errors with stack traces
|
|
76
|
+
jq 'select(.level == "ERROR" and .stack != null)' ~/.local/state/logifai/logs/current.ndjson
|
|
77
|
+
|
|
78
|
+
# Group by level
|
|
79
|
+
jq -s 'group_by(.level) | map({level: .[0].level, count: length})' ~/.local/state/logifai/logs/current.ndjson
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Investigation Workflow
|
|
83
|
+
|
|
84
|
+
When user asks about errors or issues:
|
|
85
|
+
1. Check `current.ndjson` for recent ERROR entries
|
|
86
|
+
2. Look for stack traces (`.stack != null`)
|
|
87
|
+
3. Check surrounding WARN/INFO entries for context
|
|
88
|
+
4. Search across sessions if not found in current
|
|
89
|
+
|
|
90
|
+
When user asks "what went wrong":
|
|
91
|
+
1. Search for ERROR level logs in current session
|
|
92
|
+
2. Check for stack traces
|
|
93
|
+
3. Look for related WARN logs nearby
|
|
94
|
+
4. Summarize findings with timestamps and context
|