@1claw/cli 0.57.0 → 0.58.1

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 (64) hide show
  1. package/README.md +13 -2
  2. package/dist/bundled-templates/shared/index.html +109 -0
  3. package/dist/bundled-templates/shared/spawn_chat_ui.py +343 -0
  4. package/dist/bundled-templates/shared/start-with-chat-ui.sh +33 -0
  5. package/dist/bundled-templates/templates/agentkit/Dockerfile +3 -0
  6. package/dist/bundled-templates/templates/agentkit/agent.py +1 -1
  7. package/dist/bundled-templates/templates/agentkit/entrypoint.sh +2 -2
  8. package/dist/bundled-templates/templates/agno/Dockerfile +3 -0
  9. package/dist/bundled-templates/templates/agno/agent.py +1 -1
  10. package/dist/bundled-templates/templates/agno/entrypoint.sh +2 -2
  11. package/dist/bundled-templates/templates/agno/requirements.txt +1 -1
  12. package/dist/bundled-templates/templates/coder/Dockerfile +3 -0
  13. package/dist/bundled-templates/templates/coder/agent.py +1 -1
  14. package/dist/bundled-templates/templates/coder/entrypoint.sh +2 -2
  15. package/dist/bundled-templates/templates/crewai/Dockerfile +3 -0
  16. package/dist/bundled-templates/templates/crewai/agent.py +1 -1
  17. package/dist/bundled-templates/templates/crewai/entrypoint.sh +2 -2
  18. package/dist/bundled-templates/templates/elizaos/Dockerfile +3 -0
  19. package/dist/bundled-templates/templates/elizaos/agent.ts +1 -1
  20. package/dist/bundled-templates/templates/elizaos/entrypoint.sh +2 -2
  21. package/dist/bundled-templates/templates/langchain/Dockerfile +3 -0
  22. package/dist/bundled-templates/templates/langchain/agent.py +1 -1
  23. package/dist/bundled-templates/templates/langchain/entrypoint.sh +2 -2
  24. package/dist/bundled-templates/templates/llamaindex/Dockerfile +3 -0
  25. package/dist/bundled-templates/templates/llamaindex/agent.py +1 -1
  26. package/dist/bundled-templates/templates/llamaindex/entrypoint.sh +2 -2
  27. package/dist/bundled-templates/templates/mastra/Dockerfile +4 -1
  28. package/dist/bundled-templates/templates/mastra/agent.ts +1 -1
  29. package/dist/bundled-templates/templates/mastra/entrypoint.sh +2 -2
  30. package/dist/bundled-templates/templates/openai-agents/Dockerfile +3 -0
  31. package/dist/bundled-templates/templates/openai-agents/agent.py +1 -1
  32. package/dist/bundled-templates/templates/openai-agents/entrypoint.sh +2 -2
  33. package/dist/bundled-templates/templates/pydantic-ai/Dockerfile +3 -0
  34. package/dist/bundled-templates/templates/pydantic-ai/agent.py +1 -1
  35. package/dist/bundled-templates/templates/pydantic-ai/entrypoint.sh +2 -2
  36. package/dist/bundled-templates/templates/smolagents/Dockerfile +3 -0
  37. package/dist/bundled-templates/templates/smolagents/README.md +1 -1
  38. package/dist/bundled-templates/templates/smolagents/SECURITY.md +17 -0
  39. package/dist/bundled-templates/templates/smolagents/agent.py +11 -3
  40. package/dist/bundled-templates/templates/smolagents/entrypoint.sh +2 -2
  41. package/dist/bundled-templates/templates/typescript-sdk/Dockerfile +4 -1
  42. package/dist/bundled-templates/templates/typescript-sdk/README.md +3 -0
  43. package/dist/bundled-templates/templates/typescript-sdk/agent.ts +164 -15
  44. package/dist/bundled-templates/templates/typescript-sdk/entrypoint.sh +2 -2
  45. package/dist/src/commands/agent.js +58 -0
  46. package/dist/src/commands/agent.js.map +1 -1
  47. package/dist/src/commands/init.d.ts.map +1 -1
  48. package/dist/src/commands/init.js +20 -21
  49. package/dist/src/commands/init.js.map +1 -1
  50. package/dist/src/commands/platform.js +90 -2
  51. package/dist/src/commands/platform.js.map +1 -1
  52. package/dist/src/commands/proxy.d.ts.map +1 -1
  53. package/dist/src/commands/proxy.js +7 -23
  54. package/dist/src/commands/proxy.js.map +1 -1
  55. package/dist/src/commands/spawn.d.ts.map +1 -1
  56. package/dist/src/commands/spawn.js +39 -19
  57. package/dist/src/commands/spawn.js.map +1 -1
  58. package/dist/src/lib/agent-key.d.ts +13 -0
  59. package/dist/src/lib/agent-key.d.ts.map +1 -0
  60. package/dist/src/lib/agent-key.js +45 -0
  61. package/dist/src/lib/agent-key.js.map +1 -0
  62. package/dist/src/lib/docker-client.js +1 -1
  63. package/dist/src/lib/docker-client.js.map +1 -1
  64. package/package.json +1 -1
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec npx tsx agent.ts
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh npx tsx agent.ts
@@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY requirements.txt .
10
13
  RUN pip install --no-cache-dir -r requirements.txt
11
14
 
@@ -110,6 +110,6 @@ Credentials stay in the host daemon — this container never sees secret values.
110
110
 
111
111
 
112
112
  if __name__ == "__main__":
113
- port = int(os.environ.get("CHAT_UI_PORT", "3000"))
113
+ port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
114
114
  print(f"LangChain agent listening on http://0.0.0.0:{port}")
115
115
  app.run(host="0.0.0.0", port=port)
@@ -32,5 +32,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
32
32
  fi
33
33
  fi
34
34
 
35
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
36
- exec python agent.py
35
+ # Chat UI starts in start-with-chat-ui.sh
36
+ exec /app/shared/start-with-chat-ui.sh python agent.py
@@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY requirements.txt .
10
13
  RUN pip install --no-cache-dir -r requirements.txt
11
14
 
@@ -92,6 +92,6 @@ Credentials stay in the host daemon — this container never sees secret values.
92
92
 
93
93
 
94
94
  if __name__ == "__main__":
95
- port = int(os.environ.get("CHAT_UI_PORT", "3000"))
95
+ port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
96
96
  print(f"LlamaIndex agent listening on http://0.0.0.0:{port}")
97
97
  app.run(host="0.0.0.0", port=port)
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec python agent.py
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh python agent.py
@@ -1,11 +1,14 @@
1
1
  FROM node:22-slim
2
2
 
3
3
  RUN apt-get update && apt-get install -y --no-install-recommends \
4
- curl tini && \
4
+ curl tini python3 && \
5
5
  rm -rf /var/lib/apt/lists/*
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY package.json tsconfig.json ./
10
13
  RUN npm install --omit=dev 2>/dev/null || npm install
11
14
 
@@ -63,7 +63,7 @@ async function handleChat(message: string): Promise<string> {
63
63
  return text;
64
64
  }
65
65
 
66
- const PORT = parseInt(process.env.CHAT_UI_PORT ?? "3000", 10);
66
+ const PORT = parseInt(process.env.AGENT_INTERNAL_PORT ?? "3001", 10);
67
67
 
68
68
  const server = createServer(async (req, res) => {
69
69
  if (req.url === "/health" && req.method === "GET") {
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec npx tsx agent.ts
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh npx tsx agent.ts
@@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY requirements.txt .
10
13
  RUN pip install --no-cache-dir -r requirements.txt
11
14
 
@@ -93,6 +93,6 @@ Credentials stay in the host daemon — this container never sees secret values.
93
93
 
94
94
 
95
95
  if __name__ == "__main__":
96
- port = int(os.environ.get("CHAT_UI_PORT", "3000"))
96
+ port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
97
97
  print(f"OpenAI Agents SDK agent listening on http://0.0.0.0:{port}")
98
98
  app.run(host="0.0.0.0", port=port)
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec python agent.py
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh python agent.py
@@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY requirements.txt .
10
13
  RUN pip install --no-cache-dir -r requirements.txt
11
14
 
@@ -105,6 +105,6 @@ Credentials stay in the host daemon — this container never sees secret values.
105
105
 
106
106
 
107
107
  if __name__ == "__main__":
108
- port = int(os.environ.get("CHAT_UI_PORT", "3000"))
108
+ port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
109
109
  print(f"Pydantic AI agent listening on http://0.0.0.0:{port}")
110
110
  app.run(host="0.0.0.0", port=port)
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec python agent.py
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh python agent.py
@@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY requirements.txt .
10
13
  RUN pip install --no-cache-dir -r requirements.txt
11
14
 
@@ -16,7 +16,7 @@ Or with your own API key:
16
16
 
17
17
  ## What's included
18
18
 
19
- - **agent.py** — CodeAgent with example tools (hello_world, list_env_vars)
19
+ - **agent.py** — ToolCallingAgent by default (set `ONECLAW_SMOLAGENTS_CODE_MODE=1` for CodeAgent)
20
20
  - **Flask server** on port 3000 with `/chat` and `/health` endpoints
21
21
  - **Shroud integration** — LLM calls are routed through Shroud for inspection
22
22
  - **MCP server** — started automatically for tool access to 1Claw vault
@@ -0,0 +1,17 @@
1
+ # smolagents template security
2
+
3
+ This template defaults to HuggingFace `ToolCallingAgent`, which only invokes
4
+ registered tools. It does **not** execute arbitrary Python.
5
+
6
+ `CodeAgent` (LLM-generated Python) is available behind
7
+ `ONECLAW_SMOLAGENTS_CODE_MODE=1`. Treat that mode as high-trust / development
8
+ only:
9
+
10
+ - Run it only inside the 1Claw spawn container (non-root, network-scoped).
11
+ - Do not expose the `/chat` port to the public internet.
12
+ - Route LLM traffic through Shroud (`ONECLAW_LLM_VIA_SHROUD=true`).
13
+ - Never deploy CodeAgent mode on a host that holds production credentials
14
+ outside the vault.
15
+
16
+ The 1Claw host injects secrets via the daemon; the container should not
17
+ receive raw API keys.
@@ -7,13 +7,20 @@ The container never sees raw API keys — they are injected by the host daemon.
7
7
 
8
8
  import os
9
9
  from flask import Flask, request, jsonify
10
- from smolagents import CodeAgent, tool, OpenAIServerModel
10
+ from smolagents import CodeAgent, ToolCallingAgent, tool, OpenAIServerModel
11
11
 
12
12
  app = Flask(__name__)
13
13
 
14
14
  SHROUD_URL = os.environ.get("ONECLAW_SHROUD_URL", "https://shroud.1claw.xyz")
15
15
  LLM_VIA_SHROUD = os.environ.get("ONECLAW_LLM_VIA_SHROUD", "").lower() == "true"
16
16
  MODEL = os.environ.get("ONECLAW_SHROUD_MODEL", "gpt-4o-mini")
17
+ # CodeAgent executes LLM-generated Python. Off by default; only enable inside
18
+ # an isolated container with a human-set env flag.
19
+ CODE_MODE = os.environ.get("ONECLAW_SMOLAGENTS_CODE_MODE", "").lower() in (
20
+ "1",
21
+ "true",
22
+ "yes",
23
+ )
17
24
 
18
25
 
19
26
  @tool
@@ -44,7 +51,8 @@ def create_agent():
44
51
  api_key="shroud-injected",
45
52
  )
46
53
 
47
- return CodeAgent(
54
+ agent_cls = CodeAgent if CODE_MODE else ToolCallingAgent
55
+ return agent_cls(
48
56
  tools=[hello_world, list_env_vars],
49
57
  model=model,
50
58
  )
@@ -92,6 +100,6 @@ Credentials stay in the host daemon — this container never sees secret values.
92
100
 
93
101
 
94
102
  if __name__ == "__main__":
95
- port = int(os.environ.get("CHAT_UI_PORT", "3000"))
103
+ port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
96
104
  print(f"smolagents agent listening on http://0.0.0.0:{port}")
97
105
  app.run(host="0.0.0.0", port=port)
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec python agent.py
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh python agent.py
@@ -1,11 +1,14 @@
1
1
  FROM node:22-slim
2
2
 
3
3
  RUN apt-get update && apt-get install -y --no-install-recommends \
4
- curl tini && \
4
+ curl tini python3 && \
5
5
  rm -rf /var/lib/apt/lists/*
6
6
 
7
7
  WORKDIR /app
8
8
 
9
+ COPY shared /app/shared
10
+ RUN chmod +x /app/shared/start-with-chat-ui.sh
11
+
9
12
  COPY package.json tsconfig.json ./
10
13
  RUN npm install --omit=dev 2>/dev/null || npm install
11
14
 
@@ -5,9 +5,12 @@ Plain TypeScript agent using [@1claw/sdk](https://www.npmjs.com/package/@1claw/s
5
5
  ## Quick start
6
6
 
7
7
  ```bash
8
+ # spawn starts the host daemon automatically (no separate `1claw daemon start` needed)
8
9
  1claw spawn typescript-sdk --agent-key ocv_YOUR_KEY
9
10
  ```
10
11
 
12
+ Key-only `ocv_…` auth is supported — spawn resolves your agent ID via Vault token exchange and stores Shroud credentials in the host daemon. The container routes LLM calls through the daemon `/proxy` endpoint; it never sees raw keys.
13
+
11
14
  ## What's included
12
15
 
13
16
  - `agent.ts` — TypeScript agent with `@1claw/sdk` client, AI SDK tool calling, and `/chat` + `/health` endpoints
@@ -2,10 +2,11 @@
2
2
  * Plain TypeScript agent using @1claw/sdk for vault access and signing,
3
3
  * with Vercel AI SDK for LLM interaction through Shroud.
4
4
  *
5
- * LLM calls are routed through Shroud when ONECLAW_LLM_VIA_SHROUD is set.
6
- * The container never sees raw API keys they are injected by the host daemon.
5
+ * LLM calls route through the host daemon's /proxy when ONECLAW_SHROUD_SECRET
6
+ * is set (spawn/init cloud mode). The container never sees agent or provider keys.
7
7
  */
8
8
 
9
+ import { request as httpRequest } from "node:http";
9
10
  import { createServer } from "node:http";
10
11
  import { OneclawClient } from "@1claw/sdk";
11
12
  import { createOpenAI } from "@ai-sdk/openai";
@@ -15,7 +16,12 @@ import { z } from "zod";
15
16
  const SHROUD_URL = process.env.ONECLAW_SHROUD_URL ?? "https://shroud.1claw.xyz";
16
17
  const LLM_VIA_SHROUD = process.env.ONECLAW_LLM_VIA_SHROUD === "true";
17
18
  const MODEL = process.env.ONECLAW_SHROUD_MODEL ?? "gpt-4o-mini";
18
- const PORT = parseInt(process.env.CHAT_UI_PORT ?? "3000", 10);
19
+ const PORT = parseInt(process.env.AGENT_INTERNAL_PORT ?? "3001", 10);
20
+ const DAEMON_SOCKET = process.env.ONECLAW_DAEMON_SOCKET ?? "/run/1claw/daemon.sock";
21
+ const SECRET_PREFIX = process.env.ONECLAW_SECRET_PREFIX ?? "";
22
+ const SHROUD_SECRET = process.env.ONECLAW_SHROUD_SECRET ?? "";
23
+ const SHROUD_API_KEY_SECRET = process.env.ONECLAW_SHROUD_API_KEY_SECRET ?? "";
24
+ const SHROUD_PROVIDER = process.env.ONECLAW_SHROUD_PROVIDER ?? "openai";
19
25
 
20
26
  const conversationHistory: { role: "user" | "assistant"; content: string }[] = [];
21
27
 
@@ -25,9 +31,144 @@ const claw = new OneclawClient({
25
31
  baseUrl: process.env.ONECLAW_BASE_URL ?? "https://api.1claw.xyz",
26
32
  });
27
33
 
28
- const openai = LLM_VIA_SHROUD
29
- ? createOpenAI({ baseURL: `${SHROUD_URL}/v1`, apiKey: "shroud-injected" })
30
- : null;
34
+ function daemonRequest(
35
+ method: string,
36
+ path: string,
37
+ body?: unknown,
38
+ ): Promise<{ status: number; body: Record<string, unknown> | null }> {
39
+ return new Promise((resolve, reject) => {
40
+ const payload = body ? JSON.stringify(body) : undefined;
41
+ const req = httpRequest(
42
+ {
43
+ socketPath: DAEMON_SOCKET,
44
+ path,
45
+ method,
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ ...(SECRET_PREFIX ? { "X-Secret-Prefix": SECRET_PREFIX } : {}),
49
+ ...(payload ? { "Content-Length": Buffer.byteLength(payload) } : {}),
50
+ },
51
+ timeout: 120_000,
52
+ },
53
+ (res) => {
54
+ const chunks: Buffer[] = [];
55
+ res.on("data", (c) => chunks.push(c as Buffer));
56
+ res.on("end", () => {
57
+ const text = Buffer.concat(chunks).toString("utf-8");
58
+ let parsed: Record<string, unknown> | null = null;
59
+ try {
60
+ parsed = text ? (JSON.parse(text) as Record<string, unknown>) : null;
61
+ } catch {
62
+ parsed = { raw: text };
63
+ }
64
+ resolve({ status: res.statusCode ?? 0, body: parsed });
65
+ });
66
+ },
67
+ );
68
+ req.on("error", reject);
69
+ req.on("timeout", () => req.destroy(new Error("daemon request timed out")));
70
+ if (payload) req.write(payload);
71
+ req.end();
72
+ });
73
+ }
74
+
75
+ /** Route outbound LLM HTTP through the host daemon (injects X-Shroud-Agent-Key). */
76
+ async function proxyViaDaemon(url: string, init?: RequestInit): Promise<Response> {
77
+ if (!SHROUD_SECRET) {
78
+ throw new Error(
79
+ "Shroud is enabled but ONECLAW_SHROUD_SECRET is missing. Re-run spawn with --agent-key ocv_... so the daemon can inject credentials.",
80
+ );
81
+ }
82
+
83
+ const headers: Record<string, string> = {};
84
+ if (init?.headers) {
85
+ const h = init.headers;
86
+ if (h instanceof Headers) {
87
+ h.forEach((v, k) => {
88
+ headers[k] = v;
89
+ });
90
+ } else if (Array.isArray(h)) {
91
+ for (const [k, v] of h) headers[k] = v;
92
+ } else {
93
+ Object.assign(headers, h as Record<string, string>);
94
+ }
95
+ }
96
+ delete headers.authorization;
97
+ delete headers.Authorization;
98
+ headers["X-Shroud-Provider"] = headers["X-Shroud-Provider"] ?? SHROUD_PROVIDER;
99
+
100
+ let body: string | undefined;
101
+ if (typeof init?.body === "string") {
102
+ body = init.body;
103
+ } else if (init?.body != null) {
104
+ body = String(init.body);
105
+ }
106
+
107
+ const proxyBody: Record<string, unknown> = {
108
+ secretName: SHROUD_SECRET,
109
+ url,
110
+ method: init?.method ?? "GET",
111
+ headers,
112
+ body,
113
+ };
114
+ if (SHROUD_API_KEY_SECRET) {
115
+ proxyBody.injectSecrets = [SHROUD_API_KEY_SECRET];
116
+ }
117
+
118
+ const r = await daemonRequest("POST", "/proxy", proxyBody);
119
+ if (r.status !== 200) {
120
+ const detail =
121
+ (r.body && (r.body.error as string)) ||
122
+ (r.body ? JSON.stringify(r.body) : "unknown daemon error");
123
+ throw new Error(`Daemon proxy failed (${r.status}): ${detail}`);
124
+ }
125
+
126
+ const upstreamStatus = (r.body?.status as number) ?? 502;
127
+ const upstreamBody =
128
+ typeof r.body?.body === "string"
129
+ ? r.body.body
130
+ : JSON.stringify(r.body?.body ?? {});
131
+
132
+ let errorMessage: string | undefined;
133
+ if (upstreamStatus >= 400) {
134
+ try {
135
+ const parsed = JSON.parse(upstreamBody) as {
136
+ error?: string | { message?: string };
137
+ message?: string;
138
+ };
139
+ if (typeof parsed.error === "string") {
140
+ errorMessage = parsed.error;
141
+ } else if (parsed.error && typeof parsed.error.message === "string") {
142
+ errorMessage = parsed.error.message;
143
+ } else if (typeof parsed.message === "string") {
144
+ errorMessage = parsed.message;
145
+ }
146
+ } catch {
147
+ errorMessage = upstreamBody.slice(0, 400);
148
+ }
149
+ }
150
+
151
+ if (errorMessage) {
152
+ throw new Error(errorMessage);
153
+ }
154
+
155
+ const responseHeaders = (r.body?.headers as Record<string, string>) ?? {
156
+ "Content-Type": "application/json",
157
+ };
158
+ return new Response(upstreamBody, {
159
+ status: upstreamStatus,
160
+ headers: responseHeaders,
161
+ });
162
+ }
163
+
164
+ const openai =
165
+ LLM_VIA_SHROUD && SHROUD_SECRET
166
+ ? createOpenAI({
167
+ baseURL: `${SHROUD_URL.replace(/\/+$/, "")}/v1`,
168
+ apiKey: "daemon-injected",
169
+ fetch: proxyViaDaemon as typeof fetch,
170
+ })
171
+ : null;
31
172
 
32
173
  const listSecretsTool = tool({
33
174
  description: "List secrets in the 1Claw vault (names only, no values)",
@@ -37,12 +178,14 @@ const listSecretsTool = tool({
37
178
  const vaultId = process.env.ONECLAW_VAULT_ID;
38
179
  if (!vaultId) return "No vault configured.";
39
180
  const resp = await claw.secrets.list(vaultId, prefix);
40
- const secrets = (resp as any).data?.secrets ?? [];
181
+ const secrets = (resp as { data?: { secrets?: Array<{ path: string; type?: string }> } })
182
+ .data?.secrets ?? [];
41
183
  return secrets.length
42
- ? secrets.map((s: any) => ` ${s.path} (${s.type ?? "generic"})`).join("\n")
184
+ ? secrets.map((s) => ` ${s.path} (${s.type ?? "generic"})`).join("\n")
43
185
  : "No secrets found.";
44
- } catch (e: any) {
45
- return `Error: ${e.message}`;
186
+ } catch (e: unknown) {
187
+ const msg = e instanceof Error ? e.message : String(e);
188
+ return `Error: ${msg}`;
46
189
  }
47
190
  },
48
191
  });
@@ -61,7 +204,7 @@ const envTool = tool({
61
204
 
62
205
  async function handleChat(message: string): Promise<string> {
63
206
  if (!openai) {
64
- return "No LLM configured. Use --llm-api-key or enable Token Billing.";
207
+ return "No LLM configured. Re-run spawn with --agent-key ocv_... (and optional --llm-api-key). The host daemon injects Shroud credentials.";
65
208
  }
66
209
 
67
210
  conversationHistory.push({ role: "user", content: message });
@@ -84,7 +227,12 @@ const server = createServer(async (req, res) => {
84
227
  if (req.url === "/health" && req.method === "GET") {
85
228
  res.writeHead(200, { "Content-Type": "application/json" });
86
229
  res.end(
87
- JSON.stringify({ status: "ok", framework: "typescript-sdk", llm_wired: LLM_VIA_SHROUD }),
230
+ JSON.stringify({
231
+ status: "ok",
232
+ framework: "typescript-sdk",
233
+ llm_wired: LLM_VIA_SHROUD && !!SHROUD_SECRET,
234
+ daemon_socket: DAEMON_SOCKET,
235
+ }),
88
236
  );
89
237
  return;
90
238
  }
@@ -92,7 +240,7 @@ const server = createServer(async (req, res) => {
92
240
  if (req.url === "/chat" && req.method === "POST") {
93
241
  const chunks: Buffer[] = [];
94
242
  for await (const chunk of req) chunks.push(chunk as Buffer);
95
- const body = JSON.parse(Buffer.concat(chunks).toString());
243
+ const body = JSON.parse(Buffer.concat(chunks).toString()) as { message?: string };
96
244
  const message = (body.message ?? "").trim();
97
245
 
98
246
  if (!message) {
@@ -105,9 +253,10 @@ const server = createServer(async (req, res) => {
105
253
  const response = await handleChat(message);
106
254
  res.writeHead(200, { "Content-Type": "application/json" });
107
255
  res.end(JSON.stringify({ response, framework: "typescript-sdk" }));
108
- } catch (e: any) {
256
+ } catch (e: unknown) {
257
+ const msg = e instanceof Error ? e.message : String(e);
109
258
  res.writeHead(500, { "Content-Type": "application/json" });
110
- res.end(JSON.stringify({ error: e.message }));
259
+ res.end(JSON.stringify({ error: msg }));
111
260
  }
112
261
  return;
113
262
  }
@@ -30,5 +30,5 @@ if [ -S "$DAEMON_SOCKET" ]; then
30
30
  fi
31
31
  fi
32
32
 
33
- echo "Ready: http://0.0.0.0:${CHAT_UI_PORT}"
34
- exec npx tsx agent.ts
33
+ # Chat UI starts in start-with-chat-ui.sh
34
+ exec /app/shared/start-with-chat-ui.sh npx tsx agent.ts
@@ -1738,4 +1738,62 @@ accountsCommand
1738
1738
  }
1739
1739
  });
1740
1740
  registerAgentBindingCommands(agentCommand);
1741
+ const automationAgentCommand = agentCommand
1742
+ .command("automation")
1743
+ .description("Agent-scoped automation commands (requires agent JWT)");
1744
+ automationAgentCommand
1745
+ .command("create <agent-id> <name>")
1746
+ .description("Create a simple automation for the calling agent (manual/webhook; log/notify/memory/wait steps)")
1747
+ .option("--trigger <type>", "manual or webhook", "manual")
1748
+ .option("--workflow <json-or-file>", "Workflow spec JSON or @file path", '{"steps":[{"type":"log","message":"hello"}]}')
1749
+ .option("--auto-trigger", "Trigger immediately after create (manual only)")
1750
+ .option("--token <jwt>", "Agent JWT (or set ONECLAW_AGENT_TOKEN)")
1751
+ .option("--api-key <key>", "Agent API key to exchange (or ONECLAW_AGENT_API_KEY)")
1752
+ .option("--json", "Output as JSON")
1753
+ .action(async (agentId, name, opts) => {
1754
+ try {
1755
+ let agentToken = opts.token ?? process.env.ONECLAW_AGENT_TOKEN ?? "";
1756
+ if (!agentToken) {
1757
+ const apiKey = opts.apiKey ?? process.env.ONECLAW_AGENT_API_KEY ?? "";
1758
+ if (!apiKey) {
1759
+ throw new Error("Provide --token, ONECLAW_AGENT_TOKEN, --api-key, or ONECLAW_AGENT_API_KEY");
1760
+ }
1761
+ const exchanged = await api("/auth/agent-token", {
1762
+ method: "POST",
1763
+ body: { agent_id: agentId, api_key: apiKey },
1764
+ token: undefined,
1765
+ });
1766
+ agentToken = exchanged.access_token;
1767
+ }
1768
+ let workflowSpec;
1769
+ if (opts.workflow.startsWith("@")) {
1770
+ workflowSpec = JSON.parse(readFileSync(opts.workflow.slice(1), "utf8"));
1771
+ }
1772
+ else {
1773
+ workflowSpec = JSON.parse(opts.workflow);
1774
+ }
1775
+ const result = await api(`/agents/${agentId}/automations`, {
1776
+ method: "POST",
1777
+ token: agentToken,
1778
+ body: {
1779
+ name,
1780
+ trigger_type: opts.trigger,
1781
+ workflow_spec: workflowSpec,
1782
+ auto_trigger: Boolean(opts.autoTrigger),
1783
+ },
1784
+ });
1785
+ const automation = (result.automation ?? result);
1786
+ if (opts.json) {
1787
+ printJson(automation);
1788
+ return;
1789
+ }
1790
+ printSuccess(`Agent automation created: ${String(automation.name ?? name)} (${String(automation.id ?? "")})`);
1791
+ if (typeof result.webhook_url === "string") {
1792
+ printKeyValue([["Webhook URL", result.webhook_url]]);
1793
+ }
1794
+ }
1795
+ catch (err) {
1796
+ handleError(err);
1797
+ }
1798
+ });
1741
1799
  //# sourceMappingURL=agent.js.map