@bytesbrains/pi-docker-logs 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/AGENTS.md +72 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/package.json +51 -0
- package/src/config.ts +28 -0
- package/src/helpers.ts +118 -0
- package/src/index.ts +14 -0
- package/src/tools/logs.ts +237 -0
- package/src/types.ts +12 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Docker Logs — Agent Usage Guide
|
|
2
|
+
|
|
3
|
+
> You are an AI agent. Use docker-logs tools to fetch complete raw logs from Docker containers.
|
|
4
|
+
> For label-filtered structured logs, use loki-gate tools.
|
|
5
|
+
|
|
6
|
+
## Quickstart
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
docker_container_list() # See running containers
|
|
10
|
+
docker_worker_logs(worker="1") # Tail worker 1 logs
|
|
11
|
+
docker_container_logs(container="ai-factory-orchestrator") # Tail any container
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
### docker_container_list
|
|
17
|
+
|
|
18
|
+
List running factory containers with type indicators.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
docker_container_list()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### docker_worker_logs
|
|
25
|
+
|
|
26
|
+
Fetch complete logs from a worker container. Resolves worker numbers (1-4, "worker-2") to container names automatically. Captures **all** output including `console.log` and structured JSON.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
docker_worker_logs(worker="1")
|
|
30
|
+
docker_worker_logs(worker="2", tail=500, search="error")
|
|
31
|
+
docker_worker_logs(worker="3", since="30m")
|
|
32
|
+
docker_worker_logs(worker="4", search="complete")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### docker_container_logs
|
|
36
|
+
|
|
37
|
+
Fetch complete logs from any Docker container by name. Use for orchestrator, Gitea, Postgres, or any container.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
docker_container_logs(container="ai-factory-orchestrator")
|
|
41
|
+
docker_container_logs(container="ai-factory-gitea", tail=100)
|
|
42
|
+
docker_container_logs(container="ai-factory-worker-1", search="exit=")
|
|
43
|
+
docker_container_logs(container="ai-factory-postgres", since="1h")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Docker vs Loki
|
|
47
|
+
|
|
48
|
+
| | Docker (`docker_*`) | Loki (`loki_*`) |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| **Captures** | All stdout/stderr (console.log + JSON) | Structured JSON logs only |
|
|
51
|
+
| **Per-container** | ✅ Exact container targeting | ❌ No container_name label |
|
|
52
|
+
| **Label filtering** | ❌ Text search only (`search` param) | ✅ Filter by agent, service |
|
|
53
|
+
| **Best for** | Complete job/worker output, console messages | Exploring by service/agent, error aggregation |
|
|
54
|
+
| **Requires** | Docker CLI + socket access | Loki running (port 3100) |
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
`.dockerlogs.yml`:
|
|
59
|
+
|
|
60
|
+
| Key | Default | Description |
|
|
61
|
+
|-----|---------|-------------|
|
|
62
|
+
| `tail` | `200` | Default number of log lines to tail |
|
|
63
|
+
|
|
64
|
+
## Troubleshooting
|
|
65
|
+
|
|
66
|
+
| Problem | Solution |
|
|
67
|
+
|---------|----------|
|
|
68
|
+
| `docker_container_list()` fails | Docker daemon may not be running. Run `docker ps` manually. |
|
|
69
|
+
| Container not found | Use `docker_container_list()` to see exact names. |
|
|
70
|
+
| Empty logs | Try increasing `tail` or using `since` to widen the window. |
|
|
71
|
+
| Permission denied | Ensure current user has Docker access (`docker ps` works). |
|
|
72
|
+
| Need label filtering | For agent/service filtering, use `loki_query` or `loki_worker_logs`. |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nandal
|
|
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,71 @@
|
|
|
1
|
+
# pi-docker-logs
|
|
2
|
+
|
|
3
|
+
Docker log gateway for pi agents — tail complete raw logs from Docker containers using the `docker` CLI. Works with the [wrok.in](https://github.com/nandal/wrok.in) AI Factory.
|
|
4
|
+
|
|
5
|
+
For label-filtered structured logs (by agent/service), use [pi-loki-gate](../loki-gate).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:pi-docker-logs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Add a `.dockerlogs.yml` to your repo root:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
tail: 200
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Key | Default | Description |
|
|
22
|
+
|-----|---------|-------------|
|
|
23
|
+
| `tail` | `200` | Default number of log lines to tail |
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
### docker_container_list
|
|
28
|
+
|
|
29
|
+
List running factory containers.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
docker_container_list()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### docker_worker_logs
|
|
36
|
+
|
|
37
|
+
Fetch complete logs from a factory worker container. Captures all output (console.log + structured JSON).
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
docker_worker_logs(worker="1")
|
|
41
|
+
docker_worker_logs(worker="2", tail=500, search="error")
|
|
42
|
+
docker_worker_logs(worker="3", since="30m")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### docker_container_logs
|
|
46
|
+
|
|
47
|
+
Fetch complete logs from any Docker container.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
docker_container_logs(container="ai-factory-orchestrator")
|
|
51
|
+
docker_container_logs(container="ai-factory-worker-1", search="exit=")
|
|
52
|
+
docker_container_logs(container="ai-factory-gitea", tail=100, since="1h")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Docker vs Loki Logs
|
|
56
|
+
|
|
57
|
+
| | Docker Logs | Loki |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Captures | All stdout/stderr | Structured JSON only |
|
|
60
|
+
| Per-container | ✅ | ❌ |
|
|
61
|
+
| Label filtering | ❌ (text search) | ✅ (agent, service) |
|
|
62
|
+
| Best for | Complete job output, console messages | Service/agent exploration, error patterns |
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Docker CLI (`docker`) must be installed and on PATH
|
|
67
|
+
- Current user must have Docker socket access
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytesbrains/pi-docker-logs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Docker log gateway for pi agents — tail logs from Docker containers in the wrok.in AI Factory via the docker CLI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"docker",
|
|
9
|
+
"logs",
|
|
10
|
+
"logging",
|
|
11
|
+
"ai-factory"
|
|
12
|
+
],
|
|
13
|
+
"author": "nandal <nandal@users.noreply.github.com>",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/nandal/pi-ext.git",
|
|
17
|
+
"directory": "docker-logs"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/nandal/pi-ext/tree/main/docker-logs",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/nandal/pi-ext/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"main": "./src/index.ts",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"AGENTS.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
36
|
+
"typebox": "*"
|
|
37
|
+
},
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./src/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^6.0.3",
|
|
49
|
+
"vitest": "^2.1.9"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { DockerLogsConfig } from "./types";
|
|
4
|
+
import { DEFAULT_CONFIG } from "./types";
|
|
5
|
+
|
|
6
|
+
export function loadConfig(cwd: string): DockerLogsConfig {
|
|
7
|
+
const configPath = path.join(cwd, ".dockerlogs.yml");
|
|
8
|
+
if (!fs.existsSync(configPath)) return { ...DEFAULT_CONFIG };
|
|
9
|
+
try {
|
|
10
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
11
|
+
const result: Record<string, unknown> = {};
|
|
12
|
+
for (const line of content.split("\n")) {
|
|
13
|
+
const m = line.match(/^\s*(\w[\w.]*):\s*(.+)$/);
|
|
14
|
+
if (m) {
|
|
15
|
+
let val = m[2].trim();
|
|
16
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
17
|
+
val = val.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
result[m[1]] = val;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
tail: parseInt(result["tail"] as string) || DEFAULT_CONFIG.tail,
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return { ...DEFAULT_CONFIG };
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import type { DockerLogsConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
// Known factory container names
|
|
5
|
+
const FACTORY_CONTAINERS = [
|
|
6
|
+
"ai-factory-orchestrator",
|
|
7
|
+
"ai-factory-worker-1",
|
|
8
|
+
"ai-factory-worker-2",
|
|
9
|
+
"ai-factory-worker-3",
|
|
10
|
+
"ai-factory-worker-4",
|
|
11
|
+
"ai-factory-gitea",
|
|
12
|
+
"ai-factory-postgres",
|
|
13
|
+
"ai-factory-loki",
|
|
14
|
+
"ai-factory-grafana",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run `docker logs` for a container and return stdout.
|
|
19
|
+
*/
|
|
20
|
+
export function dockerLogs(
|
|
21
|
+
container: string,
|
|
22
|
+
tail: number,
|
|
23
|
+
since?: string,
|
|
24
|
+
): Promise<{ ok: boolean; text?: string; error?: string }> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const args = ["logs", "--timestamps"];
|
|
27
|
+
if (tail > 0) args.push("--tail", String(tail));
|
|
28
|
+
if (since) args.push("--since", since);
|
|
29
|
+
args.push(container);
|
|
30
|
+
|
|
31
|
+
execFile("docker", args, { timeout: 15000, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
resolve({ ok: false, error: stderr || err.message });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
resolve({ ok: true, text: stdout });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List running Docker containers (factory-related).
|
|
43
|
+
*/
|
|
44
|
+
export function listContainers(): Promise<{ ok: boolean; containers?: string[]; error?: string }> {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
execFile(
|
|
47
|
+
"docker",
|
|
48
|
+
["ps", "--format", "{{.Names}}", "--filter", "name=ai-factory"],
|
|
49
|
+
{ timeout: 5000 },
|
|
50
|
+
(err, stdout, stderr) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
resolve({ ok: false, error: stderr || err.message });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const containers = stdout
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((c) => c.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
resolve({ ok: true, containers });
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a worker spec to a container name.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveWorkerContainer(spec: string): string | null {
|
|
69
|
+
// Already a full container name
|
|
70
|
+
if (spec.startsWith("ai-factory-")) return spec;
|
|
71
|
+
// Number or "worker-N"
|
|
72
|
+
const num = spec.replace(/^worker-/, "");
|
|
73
|
+
return `ai-factory-worker-${num}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Search log text for a pattern and return matching lines.
|
|
78
|
+
*/
|
|
79
|
+
export function grepLogs(text: string, pattern: string, contextLines: number = 0): string {
|
|
80
|
+
const lines = text.split("\n");
|
|
81
|
+
const matched: string[] = [];
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
|
|
84
|
+
if (contextLines > 0) {
|
|
85
|
+
const start = Math.max(0, i - contextLines);
|
|
86
|
+
const end = Math.min(lines.length, i + contextLines + 1);
|
|
87
|
+
if (matched.length > 0 && start <= i - contextLines + 1) {
|
|
88
|
+
// Merge overlapping ranges
|
|
89
|
+
while (matched.length > 0) matched.pop();
|
|
90
|
+
}
|
|
91
|
+
for (let j = start; j < end; j++) {
|
|
92
|
+
matched.push(lines[j]);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
matched.push(lines[i]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return matched.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Truncate log output for display.
|
|
104
|
+
*/
|
|
105
|
+
export function truncateOutput(text: string, maxLines: number): { text: string; truncated: boolean; totalLines: number } {
|
|
106
|
+
const lines = text.split("\n").filter(Boolean);
|
|
107
|
+
const totalLines = lines.length;
|
|
108
|
+
if (totalLines <= maxLines) return { text, truncated: false, totalLines };
|
|
109
|
+
|
|
110
|
+
const head = Math.ceil(maxLines * 0.2);
|
|
111
|
+
const tail = maxLines - head;
|
|
112
|
+
const truncated =
|
|
113
|
+
lines.slice(0, head).join("\n") +
|
|
114
|
+
`\n\n... [${totalLines - maxLines} lines truncated] ...\n\n` +
|
|
115
|
+
lines.slice(-tail).join("\n");
|
|
116
|
+
|
|
117
|
+
return { text: truncated, truncated: true, totalLines };
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-docker-logs — Docker Log Gateway
|
|
3
|
+
*
|
|
4
|
+
* Tools: docker_container_logs, docker_worker_logs, docker_container_list
|
|
5
|
+
* Config: .dockerlogs.yml
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { containerLogsTool, workerLogsTool, listContainersTool } from "./tools/logs";
|
|
9
|
+
|
|
10
|
+
export default function (pi: ExtensionAPI) {
|
|
11
|
+
pi.registerTool(containerLogsTool);
|
|
12
|
+
pi.registerTool(workerLogsTool);
|
|
13
|
+
pi.registerTool(listContainersTool);
|
|
14
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
import { dockerLogs, listContainers, resolveWorkerContainer, grepLogs, truncateOutput } from "../helpers";
|
|
5
|
+
|
|
6
|
+
// ─── Container Logs ────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const containerLogsTool = {
|
|
9
|
+
name: "docker_container_logs" as const,
|
|
10
|
+
label: "Get Docker Container Logs",
|
|
11
|
+
description:
|
|
12
|
+
"Fetch logs from a Docker container by name using `docker logs`. Supports tail, since, and text search filtering. Works with any container on the Docker host.",
|
|
13
|
+
parameters: Type.Object({
|
|
14
|
+
container: Type.String({
|
|
15
|
+
description: "Docker container name (e.g. 'ai-factory-worker-1', 'ai-factory-orchestrator')",
|
|
16
|
+
}),
|
|
17
|
+
tail: Type.Optional(Type.Number({ description: "Number of lines to tail (default: 200)" })),
|
|
18
|
+
since: Type.Optional(Type.String({ description: "Only logs since (e.g. '10m', '1h', ISO timestamp)" })),
|
|
19
|
+
search: Type.Optional(Type.String({ description: "Filter lines containing this text (case-insensitive)" })),
|
|
20
|
+
}),
|
|
21
|
+
async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) {
|
|
22
|
+
const config = loadConfig(ctx.cwd);
|
|
23
|
+
const tail = params.tail || config.tail;
|
|
24
|
+
|
|
25
|
+
const result = await dockerLogs(params.container, tail, params.since);
|
|
26
|
+
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: [
|
|
33
|
+
`❌ Failed to get logs for "${params.container}":`,
|
|
34
|
+
` ${result.error}`,
|
|
35
|
+
"",
|
|
36
|
+
`Check: Is Docker running? Is the container name correct?`,
|
|
37
|
+
`Try: docker_container_list() to see running containers.`,
|
|
38
|
+
].join("\n"),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
isError: true,
|
|
42
|
+
details: { container: params.container },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let text = result.text!;
|
|
47
|
+
let grepInfo = "";
|
|
48
|
+
|
|
49
|
+
if (params.search) {
|
|
50
|
+
text = grepLogs(text, params.search);
|
|
51
|
+
grepInfo = ` (filtered by "${params.search}")`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = text.split("\n").filter(Boolean);
|
|
55
|
+
if (lines.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `📋 No log entries found for "${params.container}"${grepInfo}.`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
details: { container: params.container, count: 0 },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const output = truncateOutput(text, tail);
|
|
68
|
+
let header = `📋 ${params.container} — ${lines.length} lines${grepInfo}`;
|
|
69
|
+
if (output.truncated) {
|
|
70
|
+
header += ` (showing truncated view, total: ${output.totalLines} lines)`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: `${header}\n\n${output.text}` }],
|
|
75
|
+
details: {
|
|
76
|
+
container: params.container,
|
|
77
|
+
count: lines.length,
|
|
78
|
+
totalLines: output.totalLines,
|
|
79
|
+
truncated: output.truncated,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ─── Worker Logs ───────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export const workerLogsTool = {
|
|
88
|
+
name: "docker_worker_logs" as const,
|
|
89
|
+
label: "Get Worker Container Logs",
|
|
90
|
+
description:
|
|
91
|
+
"Fetch logs from a wrok.in factory worker container. Resolves worker numbers (1-4) to container names automatically.",
|
|
92
|
+
parameters: Type.Object({
|
|
93
|
+
worker: Type.String({
|
|
94
|
+
description: 'Worker identifier: "1" through "4", "worker-2", or full container name',
|
|
95
|
+
}),
|
|
96
|
+
tail: Type.Optional(Type.Number({ description: "Number of lines to tail (default: 200)" })),
|
|
97
|
+
since: Type.Optional(Type.String({ description: "Only logs since (e.g. '10m', '1h')" })),
|
|
98
|
+
search: Type.Optional(Type.String({ description: "Filter lines containing this text" })),
|
|
99
|
+
}),
|
|
100
|
+
async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) {
|
|
101
|
+
const config = loadConfig(ctx.cwd);
|
|
102
|
+
const container = resolveWorkerContainer(params.worker);
|
|
103
|
+
|
|
104
|
+
if (!container) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: `❌ Could not resolve worker "${params.worker}" to a container name.\n Try: "1", "worker-2", or "ai-factory-worker-3".`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
isError: true,
|
|
113
|
+
details: { worker: params.worker },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tail = params.tail || config.tail;
|
|
118
|
+
const result = await dockerLogs(container, tail, params.since);
|
|
119
|
+
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: [
|
|
126
|
+
`❌ Failed to get logs for worker "${params.worker}" (container: ${container}):`,
|
|
127
|
+
` ${result.error}`,
|
|
128
|
+
].join("\n"),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
isError: true,
|
|
132
|
+
details: { worker: params.worker, container },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let text = result.text!;
|
|
137
|
+
let grepInfo = "";
|
|
138
|
+
|
|
139
|
+
if (params.search) {
|
|
140
|
+
text = grepLogs(text, params.search);
|
|
141
|
+
grepInfo = ` (filtered by "${params.search}")`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lines = text.split("\n").filter(Boolean);
|
|
145
|
+
if (lines.length === 0) {
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `📋 No log entries for worker ${params.worker}${grepInfo}.`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
details: { worker: params.worker, container, count: 0 },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const output = truncateOutput(text, tail);
|
|
158
|
+
let header = `📋 Worker ${params.worker} (${container}) — ${lines.length} lines${grepInfo}`;
|
|
159
|
+
if (output.truncated) {
|
|
160
|
+
header += ` (showing truncated view, total: ${output.totalLines} lines)`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `${header}\n\n${output.text}` }],
|
|
165
|
+
details: {
|
|
166
|
+
worker: params.worker,
|
|
167
|
+
container,
|
|
168
|
+
count: lines.length,
|
|
169
|
+
totalLines: output.totalLines,
|
|
170
|
+
truncated: output.truncated,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ─── List Containers ───────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export const listContainersTool = {
|
|
179
|
+
name: "docker_container_list" as const,
|
|
180
|
+
label: "List Factory Containers",
|
|
181
|
+
description:
|
|
182
|
+
"List running Docker containers related to the AI factory. Shows container names so you know what to pass to docker_container_logs().",
|
|
183
|
+
parameters: Type.Object({}),
|
|
184
|
+
async execute(_id: string, _params: any, _s: any, _u: any, ctx: ExtensionContext) {
|
|
185
|
+
const result = await listContainers();
|
|
186
|
+
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
return {
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: `❌ Failed to list containers: ${result.error}`,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
isError: true,
|
|
196
|
+
details: {},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const containers = result.containers!;
|
|
201
|
+
|
|
202
|
+
if (containers.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: "No factory containers running. Is the factory started? (`docker compose up -d` in the wrok.in directory)",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
details: { count: 0 },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lines = [`🐳 Running Factory Containers (${containers.length})`, ""];
|
|
215
|
+
for (const c of containers) {
|
|
216
|
+
const type = c.includes("worker")
|
|
217
|
+
? "🔵 Worker"
|
|
218
|
+
: c.includes("orchestrator")
|
|
219
|
+
? "🟡 Orchestrator"
|
|
220
|
+
: c.includes("gitea")
|
|
221
|
+
? "📦 Gitea"
|
|
222
|
+
: c.includes("postgres")
|
|
223
|
+
? "🗄️ Postgres"
|
|
224
|
+
: c.includes("loki")
|
|
225
|
+
? "📊 Loki"
|
|
226
|
+
: c.includes("grafana")
|
|
227
|
+
? "📈 Grafana"
|
|
228
|
+
: "⚪ Other";
|
|
229
|
+
lines.push(` ${type} ${c}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
234
|
+
details: { containers, count: containers.length },
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
};
|