@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 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
+ };
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * pi-docker-logs — Types
3
+ */
4
+
5
+ export interface DockerLogsConfig {
6
+ /** Default number of lines to tail */
7
+ tail: number;
8
+ }
9
+
10
+ export const DEFAULT_CONFIG: DockerLogsConfig = {
11
+ tail: 200,
12
+ };