create-ironclaws 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.
Files changed (80) hide show
  1. package/README.md +101 -0
  2. package/bin/create.js +394 -0
  3. package/package.json +33 -0
  4. package/template/.env.example +38 -0
  5. package/template/CLAUDE.md +104 -0
  6. package/template/agent-credentials.yaml +33 -0
  7. package/template/agents.yaml +22 -0
  8. package/template/container/Dockerfile +70 -0
  9. package/template/container/Dockerfile.argus +34 -0
  10. package/template/container/agent-runner/package-lock.json +1524 -0
  11. package/template/container/agent-runner/package.json +23 -0
  12. package/template/container/agent-runner/src/index.ts +630 -0
  13. package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
  14. package/template/container/agent-runner/tsconfig.json +15 -0
  15. package/template/container/build-argus.sh +25 -0
  16. package/template/container/build.sh +23 -0
  17. package/template/container/skills/agent-browser/SKILL.md +159 -0
  18. package/template/container/skills/agent-status/SKILL.md +69 -0
  19. package/template/container/skills/capabilities/SKILL.md +100 -0
  20. package/template/container/skills/edit-agent/SKILL.md +93 -0
  21. package/template/container/skills/slack-formatting/SKILL.md +92 -0
  22. package/template/container/skills/status/SKILL.md +104 -0
  23. package/template/container/tools/elastic_query.py +161 -0
  24. package/template/container/tools/gdrive_tool.py +185 -0
  25. package/template/container/tools/jira_tool.py +433 -0
  26. package/template/container/tools/slack_history_tool.py +144 -0
  27. package/template/container/tools/youtube_tool.py +174 -0
  28. package/template/docker-compose.yml +54 -0
  29. package/template/docs/how-it-works.md +496 -0
  30. package/template/eslint.config.js +32 -0
  31. package/template/groups/forge/CLAUDE.md +107 -0
  32. package/template/package-lock.json +5278 -0
  33. package/template/package.json +52 -0
  34. package/template/scripts/github-app-token.py +58 -0
  35. package/template/scripts/register-expense-agent.sh +121 -0
  36. package/template/scripts/run-migrations.ts +105 -0
  37. package/template/scripts/setup-onecli-secrets.sh +252 -0
  38. package/template/setup-agents.sh +142 -0
  39. package/template/src/channels/index.ts +13 -0
  40. package/template/src/channels/registry.test.ts +42 -0
  41. package/template/src/channels/registry.ts +28 -0
  42. package/template/src/channels/slack.test.ts +859 -0
  43. package/template/src/channels/slack.ts +373 -0
  44. package/template/src/claw-skill.test.ts +45 -0
  45. package/template/src/config.ts +94 -0
  46. package/template/src/container-runner.test.ts +221 -0
  47. package/template/src/container-runner.ts +1029 -0
  48. package/template/src/container-runtime.test.ts +149 -0
  49. package/template/src/container-runtime.ts +124 -0
  50. package/template/src/db-migration.test.ts +67 -0
  51. package/template/src/db.test.ts +484 -0
  52. package/template/src/db.ts +837 -0
  53. package/template/src/env.ts +42 -0
  54. package/template/src/formatting.test.ts +294 -0
  55. package/template/src/github-token.ts +48 -0
  56. package/template/src/google-token.ts +75 -0
  57. package/template/src/group-folder.test.ts +43 -0
  58. package/template/src/group-folder.ts +44 -0
  59. package/template/src/group-queue.test.ts +484 -0
  60. package/template/src/group-queue.ts +363 -0
  61. package/template/src/http-server.ts +343 -0
  62. package/template/src/index.ts +960 -0
  63. package/template/src/ipc-auth.test.ts +679 -0
  64. package/template/src/ipc.ts +548 -0
  65. package/template/src/logger.ts +16 -0
  66. package/template/src/mount-security.ts +421 -0
  67. package/template/src/network-policy.ts +119 -0
  68. package/template/src/remote-control.test.ts +397 -0
  69. package/template/src/remote-control.ts +224 -0
  70. package/template/src/router.ts +52 -0
  71. package/template/src/routing.test.ts +170 -0
  72. package/template/src/sender-allowlist.test.ts +216 -0
  73. package/template/src/sender-allowlist.ts +128 -0
  74. package/template/src/task-scheduler.test.ts +129 -0
  75. package/template/src/task-scheduler.ts +290 -0
  76. package/template/src/timezone.test.ts +73 -0
  77. package/template/src/timezone.ts +37 -0
  78. package/template/src/types.ts +114 -0
  79. package/template/src/worktree.ts +206 -0
  80. package/template/tsconfig.json +20 -0
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: capabilities
3
+ description: Show what this NanoClaw instance can do — installed skills, available tools, and system info. Read-only. Use when the user asks what the bot can do, what's installed, or runs /capabilities.
4
+ ---
5
+
6
+ # /capabilities — System Capabilities Report
7
+
8
+ Generate a structured read-only report of what this NanoClaw instance can do.
9
+
10
+ **Main-channel check:** Only the main channel has `/workspace/project` mounted. Run:
11
+
12
+ ```bash
13
+ test -d /workspace/project && echo "MAIN" || echo "NOT_MAIN"
14
+ ```
15
+
16
+ If `NOT_MAIN`, respond with:
17
+ > This command is available in your main chat only. Send `/capabilities` there to see what I can do.
18
+
19
+ Then stop — do not generate the report.
20
+
21
+ ## How to gather the information
22
+
23
+ Run these commands and compile the results into the report format below.
24
+
25
+ ### 1. Installed skills
26
+
27
+ List skill directories available to you:
28
+
29
+ ```bash
30
+ ls -1 /home/node/.claude/skills/ 2>/dev/null || echo "No skills found"
31
+ ```
32
+
33
+ Each directory is an installed skill. The directory name is the skill name (e.g., `agent-browser` → `/agent-browser`).
34
+
35
+ ### 2. Available tools
36
+
37
+ Read the allowed tools from your SDK configuration. You always have access to:
38
+ - **Core:** Bash, Read, Write, Edit, Glob, Grep
39
+ - **Web:** WebSearch, WebFetch
40
+ - **Orchestration:** Task, TaskOutput, TaskStop, TeamCreate, TeamDelete, SendMessage (⚠️ for sub-agent teams only — does NOT send to Slack users)
41
+ - **Other:** TodoWrite, ToolSearch, Skill, NotebookEdit
42
+ - **MCP:** mcp__nanoclaw__* (messaging, tasks, group management)
43
+
44
+ ### 3. MCP server tools
45
+
46
+ The NanoClaw MCP server exposes these tools (via `mcp__nanoclaw__*` prefix):
47
+ - `send_message` — send a message to the user/group
48
+ - `schedule_task` — schedule a recurring or one-time task
49
+ - `list_tasks` — list scheduled tasks
50
+ - `pause_task` — pause a scheduled task
51
+ - `resume_task` — resume a paused task
52
+ - `cancel_task` — cancel and delete a task
53
+ - `update_task` — update an existing task
54
+ - `register_group` — register a new chat/group (main only)
55
+
56
+ ### 4. Container tools (Python tools)
57
+
58
+ Check for Python tools available in the container:
59
+
60
+ ```bash
61
+ ls /workspace/extra/tools/*.py 2>/dev/null || echo "No extra tools found"
62
+ ```
63
+
64
+ ### 5. Group info
65
+
66
+ ```bash
67
+ ls /workspace/group/CLAUDE.md 2>/dev/null && echo "Group memory: yes" || echo "Group memory: no"
68
+ ls /workspace/extra/ 2>/dev/null && echo "Extra mounts: $(ls /workspace/extra/ 2>/dev/null | wc -l | tr -d ' ')" || echo "Extra mounts: none"
69
+ ```
70
+
71
+ ## Report format
72
+
73
+ Present the report as a clean, readable message. Example:
74
+
75
+ ```
76
+ 📋 *NanoClaw Capabilities*
77
+
78
+ *Installed Skills:*
79
+ • /agent-browser — Browse the web, fill forms, extract data
80
+ • /capabilities — This report
81
+ (list all found skills)
82
+
83
+ *Tools:*
84
+ • Core: Bash, Read, Write, Edit, Glob, Grep
85
+ • Web: WebSearch, WebFetch
86
+ • Orchestration: Task, TeamCreate, SendMessage
87
+ • MCP: send_message, schedule_task, list_tasks, pause/resume/cancel/update_task, register_group
88
+
89
+ *Container Tools:*
90
+ • agent-browser: ✓
91
+
92
+ *System:*
93
+ • Group memory: yes/no
94
+ • Extra mounts: N directories
95
+ • Main channel: yes
96
+ ```
97
+
98
+ Adapt the output based on what you actually find — don't list things that aren't installed.
99
+
100
+ **See also:** `/status` for a quick health check of session, workspace, and tasks.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: edit-agent
3
+ description: Collaboratively edit any agent's CLAUDE.md — propose a diff, wait for confirmation, then apply. Use when asked to update, change, or improve an agent's instructions or behaviour.
4
+ ---
5
+
6
+ # Edit Agent — Collaborative CLAUDE.md Editor
7
+
8
+ Use this when the user wants to change how an agent behaves.
9
+
10
+ ## Step 1 — Identify the target agent
11
+
12
+ Figure out which agent to edit from the user's message. If ambiguous, ask which one.
13
+
14
+ Get the folder name:
15
+
16
+ ```bash
17
+ python3 -c "
18
+ import sqlite3
19
+ db = sqlite3.connect('/workspace/project/store/messages.db')
20
+ rows = db.execute('SELECT name, folder FROM registered_groups').fetchall()
21
+ for r in rows:
22
+ print(f'{r[0]} -> {r[1]}')
23
+ db.close()
24
+ "
25
+ ```
26
+
27
+ ## Step 2 — Read the current CLAUDE.md
28
+
29
+ ```bash
30
+ cat /workspace/project/groups/<folder>/CLAUDE.md
31
+ ```
32
+
33
+ ## Step 3 — Understand what needs to change
34
+
35
+ Read the current file carefully. Understand the full context before proposing anything.
36
+
37
+ ## Step 4 — Propose the change
38
+
39
+ Show the user EXACTLY what will change. Be specific — quote the text being removed and the text replacing it, or show where new content will be inserted.
40
+
41
+ Format:
42
+
43
+ ```
44
+ Here's what I'm proposing to change in *<Agent Name>*'s CLAUDE.md:
45
+
46
+ *Remove:*
47
+ > <exact text being removed>
48
+
49
+ *Replace with:*
50
+ > <exact new text>
51
+
52
+ *Reason:* <one sentence explaining why>
53
+
54
+ Should I go ahead?
55
+ ```
56
+
57
+ If it's an addition rather than a replacement:
58
+
59
+ ```
60
+ Here's what I'm proposing to add to *<Agent Name>*'s CLAUDE.md:
61
+
62
+ *Add after "<section heading>":*
63
+ > <exact new text>
64
+
65
+ Should I go ahead?
66
+ ```
67
+
68
+ ## Step 5 — Wait for explicit confirmation
69
+
70
+ Do NOT apply the change until the user says yes / go ahead / confirmed / approved or similar.
71
+
72
+ If they say no or want adjustments — revise and re-propose.
73
+
74
+ ## Step 6 — Apply the change
75
+
76
+ ```bash
77
+ # Read current content, apply edit, write back
78
+ # Use Python for safe file manipulation
79
+ python3 -c "
80
+ content = open('/workspace/extra/groups/<folder>/CLAUDE.md').read()
81
+ # Apply the specific change
82
+ content = content.replace('<old text>', '<new text>')
83
+ open('/workspace/extra/groups/<folder>/CLAUDE.md', 'w').write(content)
84
+ print('Done')
85
+ "
86
+ ```
87
+
88
+ ## Step 7 — Confirm
89
+
90
+ Tell the user:
91
+ - What was changed
92
+ - That the change is live immediately (no restart needed for CLAUDE.md changes)
93
+ - The agent will use the new instructions from its next message
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: slack-formatting
3
+ description: Format messages for Slack using mrkdwn syntax. Use when responding to Slack channels (folder starts with "slack_" or JID contains slack identifiers).
4
+ ---
5
+
6
+ # Slack Message Formatting (mrkdwn)
7
+
8
+ When responding to Slack channels, use Slack's mrkdwn syntax instead of standard Markdown.
9
+
10
+ ## Slack context
11
+
12
+ All agents in this deployment communicate via Slack. These formatting rules always apply.
13
+
14
+ ## Formatting reference
15
+
16
+ ### Text styles
17
+
18
+ | Style | Syntax | Example |
19
+ |-------|--------|---------|
20
+ | Bold | `*text*` | *bold text* |
21
+ | Italic | `_text_` | _italic text_ |
22
+ | Strikethrough | `~text~` | ~strikethrough~ |
23
+ | Code (inline) | `` `code` `` | `inline code` |
24
+ | Code block | ` ```code``` ` | Multi-line code |
25
+
26
+ ### Links and mentions
27
+
28
+ ```
29
+ <https://example.com|Link text> # Named link
30
+ <https://example.com> # Auto-linked URL
31
+ <@U1234567890> # Mention user by ID
32
+ <#C1234567890> # Mention channel by ID
33
+ <!here> # @here
34
+ <!channel> # @channel
35
+ ```
36
+
37
+ ### Lists
38
+
39
+ Slack supports simple bullet lists but NOT numbered lists:
40
+
41
+ ```
42
+ • First item
43
+ • Second item
44
+ • Third item
45
+ ```
46
+
47
+ Use `•` (bullet character) or `- ` or `* ` for bullets.
48
+
49
+ ### Block quotes
50
+
51
+ ```
52
+ > This is a block quote
53
+ > It can span multiple lines
54
+ ```
55
+
56
+ ### Emoji
57
+
58
+ Use standard emoji shortcodes: `:white_check_mark:`, `:x:`, `:rocket:`, `:tada:`
59
+
60
+ ## What NOT to use
61
+
62
+ - **NO** `##` headings (use `*Bold text*` for headers instead)
63
+ - **NO** `**double asterisks**` for bold (use `*single asterisks*`)
64
+ - **NO** `[text](url)` links (use `<url|text>` instead)
65
+ - **NO** `1.` numbered lists (use bullets with numbers: `• 1. First`)
66
+ - **NO** tables (use code blocks or plain text alignment)
67
+ - **NO** `---` horizontal rules
68
+
69
+ ## Example message
70
+
71
+ ```
72
+ *Daily Standup Summary*
73
+
74
+ _March 21, 2026_
75
+
76
+ • *Completed:* Fixed authentication bug in login flow
77
+ • *In Progress:* Building new dashboard widgets
78
+ • *Blocked:* Waiting on API access from DevOps
79
+
80
+ > Next sync: Monday 10am
81
+
82
+ :white_check_mark: All tests passing | <https://ci.example.com/builds/123|View Build>
83
+ ```
84
+
85
+ ## Quick rules
86
+
87
+ 1. Use `*bold*` not `**bold**`
88
+ 2. Use `<url|text>` not `[text](url)`
89
+ 3. Use `•` bullets, avoid numbered lists
90
+ 4. Use `:emoji:` shortcodes
91
+ 5. Quote blocks with `>`
92
+ 6. Skip headings — use bold text instead
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: status
3
+ description: Quick read-only health check — session context, workspace mounts, tool availability, and task snapshot. Use when the user asks for system status or runs /status.
4
+ ---
5
+
6
+ # /status — System Status Check
7
+
8
+ Generate a quick read-only status report of the current agent environment.
9
+
10
+ **Main-channel check:** Only the main channel has `/workspace/project` mounted. Run:
11
+
12
+ ```bash
13
+ test -d /workspace/project && echo "MAIN" || echo "NOT_MAIN"
14
+ ```
15
+
16
+ If `NOT_MAIN`, respond with:
17
+ > This command is available in your main chat only. Send `/status` there to check system status.
18
+
19
+ Then stop — do not generate the report.
20
+
21
+ ## How to gather the information
22
+
23
+ Run the checks below and compile results into the report format.
24
+
25
+ ### 1. Session context
26
+
27
+ ```bash
28
+ echo "Timestamp: $(date)"
29
+ echo "Working dir: $(pwd)"
30
+ echo "Channel: main"
31
+ ```
32
+
33
+ ### 2. Workspace and mount visibility
34
+
35
+ ```bash
36
+ echo "=== Workspace ==="
37
+ ls /workspace/ 2>/dev/null
38
+ echo "=== Group folder ==="
39
+ ls /workspace/group/ 2>/dev/null | head -20
40
+ echo "=== Extra mounts ==="
41
+ ls /workspace/extra/ 2>/dev/null || echo "none"
42
+ echo "=== IPC ==="
43
+ ls /workspace/ipc/ 2>/dev/null
44
+ ```
45
+
46
+ ### 3. Tool availability
47
+
48
+ Confirm which tool families are available to you:
49
+
50
+ - **Core:** Bash, Read, Write, Edit, Glob, Grep
51
+ - **Web:** WebSearch, WebFetch
52
+ - **Orchestration:** Task, TaskOutput, TaskStop, TeamCreate, TeamDelete, SendMessage
53
+ - **MCP:** mcp__nanoclaw__* (send_message, schedule_task, list_tasks, pause_task, resume_task, cancel_task, update_task, register_group)
54
+
55
+ ### 4. Container utilities
56
+
57
+ ```bash
58
+ which agent-browser 2>/dev/null && echo "agent-browser: available" || echo "agent-browser: not installed"
59
+ node --version 2>/dev/null
60
+ claude --version 2>/dev/null
61
+ ```
62
+
63
+ ### 5. Task snapshot
64
+
65
+ Use the MCP tool to list tasks:
66
+
67
+ ```
68
+ Call mcp__nanoclaw__list_tasks to get scheduled tasks.
69
+ ```
70
+
71
+ If no tasks exist, report "No scheduled tasks."
72
+
73
+ ## Report format
74
+
75
+ Present as a clean, readable message:
76
+
77
+ ```
78
+ 🔍 *NanoClaw Status*
79
+
80
+ *Session:*
81
+ • Channel: main
82
+ • Time: 2026-03-14 09:30 UTC
83
+ • Working dir: /workspace/group
84
+
85
+ *Workspace:*
86
+ • Group folder: ✓ (N files)
87
+ • Extra mounts: none / N directories
88
+ • IPC: ✓ (messages, tasks, input)
89
+
90
+ *Tools:*
91
+ • Core: ✓ Web: ✓ Orchestration: ✓ MCP: ✓
92
+
93
+ *Container:*
94
+ • agent-browser: ✓ / not installed
95
+ • Node: vXX.X.X
96
+ • Claude Code: vX.X.X
97
+
98
+ *Scheduled Tasks:*
99
+ • N active tasks / No scheduled tasks
100
+ ```
101
+
102
+ Adapt based on what you actually find. Keep it concise — this is a quick health check, not a deep diagnostic.
103
+
104
+ **See also:** `/capabilities` for a full list of installed skills and tools.
@@ -0,0 +1,161 @@
1
+ """
2
+ Standalone Elastic/Kibana log query tool for Argus.
3
+
4
+ Usage:
5
+ python3 elastic_query.py --namespace it-ops
6
+ python3 elastic_query.py --namespace it-ops --container my-service
7
+ python3 elastic_query.py --namespace it-ops --from 2024-01-01T10:00:00Z --to 2024-01-01T11:00:00Z
8
+ python3 elastic_query.py # searches all namespaces with default lookback
9
+
10
+ Environment variables (required):
11
+ ELASTIC_BASE_URL e.g. https://kibana.yourcompany.com
12
+ ELASTIC_API_KEY Kibana API key
13
+
14
+ Optional environment variables:
15
+ ELASTIC_LOOKBACK_MINUTES default 60
16
+ ELASTIC_MAX_LOGS default 100
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ from datetime import datetime, timedelta, timezone
24
+
25
+ try:
26
+ import requests
27
+ except ImportError:
28
+ print("ERROR: 'requests' is not installed. Run: pip3 install requests", file=sys.stderr)
29
+ sys.exit(1)
30
+
31
+ KIBANA_PROXY_PATH = "/api/console/proxy"
32
+
33
+
34
+ def parse_args():
35
+ p = argparse.ArgumentParser(description="Query Elastic error logs via Kibana proxy")
36
+ p.add_argument("--namespace", help="Kubernetes namespace (omit to search all)")
37
+ p.add_argument("--container", help="Kubernetes container name (optional filter)")
38
+ p.add_argument("--from", dest="from_ts", help="Start timestamp (ISO 8601 or 'now-Xm/h/d')")
39
+ p.add_argument("--to", dest="to_ts", help="End timestamp (ISO 8601 or 'now')")
40
+ p.add_argument("--lookback", type=int, help="Lookback minutes (overrides ELASTIC_LOOKBACK_MINUTES)")
41
+ p.add_argument("--max", type=int, help="Max log entries (overrides ELASTIC_MAX_LOGS)")
42
+ return p.parse_args()
43
+
44
+
45
+ def resolve_ts(ts: str) -> str:
46
+ """Resolve 'now-Xm/h/d' and 'now' to ISO timestamps."""
47
+ if not ts.startswith("now"):
48
+ return ts
49
+ import re
50
+ m = re.match(r"now-(\d+)([mhd])$", ts)
51
+ if m:
52
+ amount, unit = int(m.group(1)), m.group(2)
53
+ delta = {"m": timedelta(minutes=amount), "h": timedelta(hours=amount), "d": timedelta(days=amount)}[unit]
54
+ return (datetime.now(timezone.utc) - delta).isoformat()
55
+ return datetime.now(timezone.utc).isoformat()
56
+
57
+
58
+ def query_logs(namespace, container, from_ts, to_ts, max_logs, base_url, api_key):
59
+ if from_ts and to_ts:
60
+ time_range = {"gte": from_ts, "lte": to_ts}
61
+ else:
62
+ now = datetime.now(timezone.utc)
63
+ lookback = int(os.environ.get("ELASTIC_LOOKBACK_MINUTES", "60"))
64
+ since = now - timedelta(minutes=lookback)
65
+ time_range = {"gte": since.isoformat(), "lte": now.isoformat()}
66
+
67
+ must = [{"range": {"@timestamp": time_range}}]
68
+ if namespace:
69
+ must.append({"match_phrase": {"kubernetes.namespace": namespace}})
70
+ if container:
71
+ must.append({"match_phrase": {"kubernetes.container.name": container}})
72
+
73
+ body = {
74
+ "size": max_logs,
75
+ "sort": [{"@timestamp": {"order": "desc"}}],
76
+ "query": {
77
+ "bool": {
78
+ "must": must,
79
+ "should": [
80
+ {"match_phrase": {"log.level": "error"}},
81
+ {"match_phrase": {"log.level": "ERROR"}},
82
+ {"match": {"message": "error"}},
83
+ {"match": {"message": "exception"}},
84
+ {"match": {"message": "traceback"}},
85
+ {"match": {"message": "Traceback"}},
86
+ ],
87
+ "minimum_should_match": 1,
88
+ }
89
+ },
90
+ "_source": [
91
+ "@timestamp",
92
+ "message",
93
+ "log.level",
94
+ "kubernetes.pod.name",
95
+ "kubernetes.container.name",
96
+ ],
97
+ }
98
+
99
+ url = f"{base_url.rstrip('/')}{KIBANA_PROXY_PATH}"
100
+ params = {"path": "/filebeat-*/_search", "method": "GET"}
101
+ # Authorization header is optional here — if ELASTIC_API_KEY env var is set,
102
+ # use it directly. If absent, the OneCLI HTTPS proxy injects it automatically.
103
+ headers = {"kbn-xsrf": "true", "Content-Type": "application/json"}
104
+ if api_key:
105
+ headers["Authorization"] = f"ApiKey {api_key}"
106
+
107
+ resp = requests.post(url, params=params, headers=headers, json=body, timeout=30)
108
+ resp.raise_for_status()
109
+ hits = resp.json().get("hits", {}).get("hits", [])
110
+ return [h["_source"] for h in hits]
111
+
112
+
113
+ def format_logs(logs):
114
+ if not logs:
115
+ return "No error logs found in the specified time window."
116
+ lines = []
117
+ for log in logs:
118
+ ts = log.get("@timestamp", "?")
119
+ level = log.get("log.level") or (log.get("log") or {}).get("level", "?")
120
+ k8s = log.get("kubernetes") or {}
121
+ pod = (k8s.get("pod") or {}).get("name", "?")
122
+ ctr = (k8s.get("container") or {}).get("name", "?")
123
+ msg = log.get("message", "").strip()
124
+ lines.append(f"[{ts}] [{level}] [{ctr}/{pod}] {msg}")
125
+ return "\n".join(lines)
126
+
127
+
128
+ def main():
129
+ args = parse_args()
130
+
131
+ base_url = os.environ.get("ELASTIC_BASE_URL", "").strip()
132
+ if not base_url:
133
+ print("ERROR: ELASTIC_BASE_URL must be set in the environment.", file=sys.stderr)
134
+ sys.exit(1)
135
+
136
+ # api_key is optional — if absent, OneCLI HTTPS proxy injects the Authorization header.
137
+ api_key = os.environ.get("ELASTIC_API_KEY", "").strip()
138
+
139
+ from_ts = resolve_ts(args.from_ts) if args.from_ts else None
140
+ to_ts = resolve_ts(args.to_ts) if args.to_ts else None
141
+ max_logs = args.max or int(os.environ.get("ELASTIC_MAX_LOGS", "100"))
142
+
143
+ try:
144
+ logs = query_logs(
145
+ namespace=args.namespace,
146
+ container=args.container,
147
+ from_ts=from_ts,
148
+ to_ts=to_ts,
149
+ max_logs=max_logs,
150
+ base_url=base_url,
151
+ api_key=api_key,
152
+ )
153
+ except Exception as e:
154
+ print(f"ERROR: Elastic query failed: {e}", file=sys.stderr)
155
+ sys.exit(1)
156
+
157
+ print(format_logs(logs))
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()