@rethinkingstudio/clawpilot 2.1.21-beta.0 → 2.1.21-beta.10

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.
@@ -0,0 +1,193 @@
1
+ import { randomUUID } from "crypto";
2
+ import { homedir } from "os";
3
+ import { existsSync, statSync } from "fs";
4
+ import { spawn } from "node-pty";
5
+ export class PtyManager {
6
+ emitEvent;
7
+ activeSession = null;
8
+ constructor(emitEvent) {
9
+ this.emitEvent = emitEvent;
10
+ }
11
+ open(rawParams) {
12
+ if (this.activeSession) {
13
+ throw new Error("terminal session already active");
14
+ }
15
+ const params = isRecord(rawParams) ? rawParams : {};
16
+ const cols = clampInt(params.cols, 120, 20, 400);
17
+ const rows = clampInt(params.rows, 32, 10, 200);
18
+ const cwd = resolveWorkingDirectory(normalizeString(params.cwd));
19
+ const shellCandidates = resolveShellCandidates(normalizeString(params.shell));
20
+ const sessionId = randomUUID();
21
+ const { pty, shell } = spawnWithFallback(shellCandidates, cwd, cols, rows);
22
+ const session = {
23
+ sessionId,
24
+ shell,
25
+ cwd,
26
+ cols,
27
+ rows,
28
+ pty,
29
+ };
30
+ this.activeSession = session;
31
+ pty.onData((data) => {
32
+ if (this.activeSession?.sessionId !== sessionId)
33
+ return;
34
+ this.emitEvent("terminal.data", { sessionId, data });
35
+ });
36
+ pty.onExit(({ exitCode, signal }) => {
37
+ if (this.activeSession?.sessionId !== sessionId)
38
+ return;
39
+ this.activeSession = null;
40
+ this.emitEvent("terminal.closed", {
41
+ sessionId,
42
+ exitCode,
43
+ signal,
44
+ reason: "process_exited",
45
+ });
46
+ });
47
+ const info = stripPty(session);
48
+ this.emitEvent("terminal.opened", info);
49
+ return info;
50
+ }
51
+ input(rawParams) {
52
+ const session = this.requireSession(rawParams);
53
+ const params = isRecord(rawParams) ? rawParams : {};
54
+ const data = normalizeString(params.data);
55
+ if (!data) {
56
+ throw new Error("terminal input data is required");
57
+ }
58
+ session.pty.write(data);
59
+ }
60
+ resize(rawParams) {
61
+ const session = this.requireSession(rawParams);
62
+ const params = isRecord(rawParams) ? rawParams : {};
63
+ const cols = clampInt(params.cols, session.cols, 20, 400);
64
+ const rows = clampInt(params.rows, session.rows, 10, 200);
65
+ session.pty.resize(cols, rows);
66
+ session.cols = cols;
67
+ session.rows = rows;
68
+ return stripPty(session);
69
+ }
70
+ close(rawParams, reason = "closed_by_client") {
71
+ const session = this.requireSession(rawParams);
72
+ this.activeSession = null;
73
+ session.pty.kill();
74
+ this.emitEvent("terminal.closed", {
75
+ sessionId: session.sessionId,
76
+ reason,
77
+ });
78
+ }
79
+ shutdown(reason = "relay_disconnected") {
80
+ if (!this.activeSession)
81
+ return;
82
+ const session = this.activeSession;
83
+ this.activeSession = null;
84
+ session.pty.kill();
85
+ this.emitEvent("terminal.closed", {
86
+ sessionId: session.sessionId,
87
+ reason,
88
+ });
89
+ }
90
+ currentSession() {
91
+ return this.activeSession ? stripPty(this.activeSession) : null;
92
+ }
93
+ requireSession(rawParams) {
94
+ const params = isRecord(rawParams) ? rawParams : {};
95
+ const sessionId = normalizeString(params.sessionId);
96
+ if (!this.activeSession) {
97
+ throw new Error("terminal session is not active");
98
+ }
99
+ if (sessionId && sessionId != this.activeSession.sessionId) {
100
+ throw new Error("terminal session not found");
101
+ }
102
+ return this.activeSession;
103
+ }
104
+ }
105
+ function stripPty(session) {
106
+ return {
107
+ sessionId: session.sessionId,
108
+ shell: session.shell,
109
+ cwd: session.cwd,
110
+ cols: session.cols,
111
+ rows: session.rows,
112
+ };
113
+ }
114
+ function isRecord(value) {
115
+ return !!value && typeof value === "object" && !Array.isArray(value);
116
+ }
117
+ function normalizeString(value) {
118
+ if (typeof value !== "string") {
119
+ return undefined;
120
+ }
121
+ const trimmed = value.trim();
122
+ return trimmed.length === 0 ? undefined : trimmed;
123
+ }
124
+ function clampInt(value, fallback, min, max) {
125
+ const numeric = typeof value === "number" ? value : Number(value);
126
+ if (!Number.isFinite(numeric))
127
+ return fallback;
128
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
129
+ }
130
+ function defaultShell() {
131
+ if (process.platform === "win32") {
132
+ return "pwsh.exe";
133
+ }
134
+ return process.env.SHELL || "/bin/bash";
135
+ }
136
+ function resolveShellCandidates(requestedShell) {
137
+ if (requestedShell) {
138
+ return dedupe([requestedShell, ...platformShellFallbacks()]);
139
+ }
140
+ return dedupe([defaultShell(), ...platformShellFallbacks()]);
141
+ }
142
+ function platformShellFallbacks() {
143
+ if (process.platform === "win32") {
144
+ return ["powershell.exe", "cmd.exe"];
145
+ }
146
+ return ["/bin/zsh", "/bin/bash", "/bin/sh"];
147
+ }
148
+ function resolveWorkingDirectory(requestedCwd) {
149
+ const candidates = dedupe([
150
+ requestedCwd,
151
+ process.env.HOME,
152
+ homedir(),
153
+ process.cwd(),
154
+ "/",
155
+ ].filter((value) => !!value));
156
+ for (const candidate of candidates) {
157
+ if (directoryExists(candidate)) {
158
+ return candidate;
159
+ }
160
+ }
161
+ return process.cwd();
162
+ }
163
+ function directoryExists(path) {
164
+ try {
165
+ return existsSync(path) && statSync(path).isDirectory();
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ function spawnWithFallback(shellCandidates, cwd, cols, rows) {
172
+ let lastError;
173
+ for (const shell of shellCandidates) {
174
+ try {
175
+ const pty = spawn(shell, [], {
176
+ name: "xterm-256color",
177
+ cols,
178
+ rows,
179
+ cwd,
180
+ env: { ...process.env },
181
+ });
182
+ return { pty, shell };
183
+ }
184
+ catch (error) {
185
+ lastError = error;
186
+ }
187
+ }
188
+ throw new Error(`failed to open terminal: ${String(lastError)}; shells tried: ${shellCandidates.join(", ")}; cwd=${cwd}`);
189
+ }
190
+ function dedupe(values) {
191
+ return [...new Set(values)];
192
+ }
193
+ //# sourceMappingURL=pty-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-manager.js","sourceRoot":"","sources":["../../src/terminal/pty-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethinkingstudio/clawpilot",
3
- "version": "2.1.21-beta.0",
3
+ "version": "2.1.21-beta.10",
4
4
  "description": "ClawAI relay client for Mac mini",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,30 +87,102 @@ def read_base_config_for_profile() -> Dict[str, Any]:
87
87
  return read_yaml_config(parent.parent / "config.yaml")
88
88
 
89
89
 
90
+ def normalize_message_content(value: Any) -> Any:
91
+ if value is None:
92
+ return ""
93
+ if isinstance(value, str):
94
+ return value
95
+ if isinstance(value, list):
96
+ parts: List[Any] = []
97
+ for part in value:
98
+ if isinstance(part, str):
99
+ parts.append({"type": "text", "text": part})
100
+ continue
101
+ if not isinstance(part, dict):
102
+ continue
103
+ clean_part = {k: v for k, v in part.items() if v is not None}
104
+ if clean_part.get("type") == "text":
105
+ clean_part["text"] = str(clean_part.get("text") or "")
106
+ if clean_part:
107
+ parts.append(clean_part)
108
+ return parts if parts else ""
109
+ return str(value)
110
+
111
+
112
+ def normalize_tool_calls(value: Any) -> List[Dict[str, Any]]:
113
+ if not isinstance(value, list):
114
+ return []
115
+ clean_calls: List[Dict[str, Any]] = []
116
+ for index, call in enumerate(value):
117
+ if not isinstance(call, dict):
118
+ continue
119
+ raw_function = call.get("function")
120
+ function = raw_function if isinstance(raw_function, dict) else {}
121
+ name = str(function.get("name") or call.get("name") or "").strip()
122
+ if not name:
123
+ continue
124
+ raw_arguments = function.get("arguments")
125
+ if raw_arguments is None:
126
+ arguments = "{}"
127
+ elif isinstance(raw_arguments, str):
128
+ arguments = raw_arguments
129
+ else:
130
+ try:
131
+ arguments = json.dumps(raw_arguments, ensure_ascii=False)
132
+ except Exception:
133
+ arguments = str(raw_arguments)
134
+ call_id = str(call.get("id") or call.get("call_id") or f"call_{index}").strip()
135
+ clean_calls.append(
136
+ {
137
+ "id": call_id,
138
+ "type": str(call.get("type") or "function"),
139
+ "function": {
140
+ "name": name,
141
+ "arguments": arguments,
142
+ },
143
+ }
144
+ )
145
+ return clean_calls
146
+
147
+
90
148
  def sanitize_messages_for_api(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
91
149
  valid_tool_call_ids: set[str] = set()
92
150
  for msg in messages:
93
151
  if not isinstance(msg, dict):
94
152
  continue
95
153
  if msg.get("role") == "assistant":
96
- for tc in msg.get("tool_calls") or []:
97
- if isinstance(tc, dict):
98
- tid = tc.get("id") or tc.get("call_id") or ""
99
- if tid:
100
- valid_tool_call_ids.add(str(tid))
154
+ for tc in normalize_tool_calls(msg.get("tool_calls")):
155
+ tid = tc.get("id") or ""
156
+ if tid:
157
+ valid_tool_call_ids.add(str(tid))
101
158
 
102
159
  clean: List[Dict[str, Any]] = []
103
160
  for msg in messages:
104
161
  if not isinstance(msg, dict):
105
162
  continue
106
- role = msg.get("role")
163
+ role = str(msg.get("role") or "").strip()
164
+ if role not in {"system", "user", "assistant", "tool"}:
165
+ continue
166
+ sanitized: Dict[str, Any] = {"role": role}
107
167
  if role == "tool":
108
168
  tid = str(msg.get("tool_call_id") or "")
109
169
  if not tid or tid not in valid_tool_call_ids:
110
170
  continue
111
- sanitized = {k: v for k, v in msg.items() if k in _API_SAFE_MSG_KEYS}
112
- if sanitized.get("role"):
113
- clean.append(sanitized)
171
+ sanitized["tool_call_id"] = tid
172
+ if msg.get("name"):
173
+ sanitized["name"] = str(msg.get("name"))
174
+
175
+ if role == "assistant":
176
+ tool_calls = normalize_tool_calls(msg.get("tool_calls"))
177
+ if tool_calls:
178
+ sanitized["tool_calls"] = tool_calls
179
+
180
+ content = normalize_message_content(msg.get("content"))
181
+ if content or role != "assistant" or sanitized.get("tool_calls"):
182
+ sanitized["content"] = content
183
+ if msg.get("refusal") is not None:
184
+ sanitized["refusal"] = str(msg.get("refusal"))
185
+ clean.append(sanitized)
114
186
  return clean
115
187
 
116
188
 
@@ -147,9 +219,10 @@ def read_session_history_from_json(session_id: Optional[str]) -> List[Dict[str,
147
219
 
148
220
 
149
221
  def read_session_history(session_id: Optional[str], session_db: Optional[SessionDB]) -> List[Dict[str, Any]]:
150
- from_db = read_session_history_from_db(session_id, session_db)
151
- if from_db:
152
- return from_db
222
+ if os.environ.get("CLAWPILOT_HERMES_BRIDGE_DISABLE_SESSIONDB_HISTORY") != "1":
223
+ from_db = read_session_history_from_db(session_id, session_db)
224
+ if from_db:
225
+ return from_db
153
226
  return read_session_history_from_json(session_id)
154
227
 
155
228
 
@@ -221,6 +294,16 @@ def resolve_runtime(
221
294
  )
222
295
 
223
296
 
297
+ def resolve_toolsets(cfg: Dict[str, Any]) -> List[str]:
298
+ raw_toolsets = _get_platform_tools(cfg, "cli")
299
+ if raw_toolsets is None:
300
+ return []
301
+ try:
302
+ return [str(toolset) for toolset in raw_toolsets]
303
+ except TypeError as exc:
304
+ raise TypeError("Hermes CLI toolset config must be iterable") from exc
305
+
306
+
224
307
  def derive_agent_inputs(payload: Dict[str, Any], session_db: Optional[SessionDB] = None) -> Dict[str, Any]:
225
308
  user_message = str(payload.get("userMessage") or "").strip()
226
309
  if not user_message:
@@ -230,6 +313,8 @@ def derive_agent_inputs(payload: Dict[str, Any], session_db: Optional[SessionDB]
230
313
  session_title = str(payload.get("sessionTitle") or "").strip() or None
231
314
  model = str(payload.get("model") or "").strip() or None
232
315
  provider = str(payload.get("provider") or "").strip() or None
316
+ base_url = str(payload.get("baseUrl") or "").strip() or None
317
+ api_key = str(payload.get("apiKey") or "").strip() or None
233
318
 
234
319
  inherit_base_env_for_profile()
235
320
 
@@ -248,16 +333,18 @@ def derive_agent_inputs(payload: Dict[str, Any], session_db: Optional[SessionDB]
248
333
  if not default_model:
249
334
  raise ValueError("missing model")
250
335
 
336
+ configured_base_url = str(model_cfg.get("base_url") or "").strip() or None
251
337
  runtime = normalize_runtime_config(
252
- resolve_runtime(
253
- provider or model_cfg.get("provider"),
254
- explicit_base_url=model_cfg.get("base_url"),
338
+ resolve_runtime_provider(
339
+ requested=provider or model_cfg.get("provider"),
340
+ explicit_api_key=api_key,
341
+ explicit_base_url=base_url or configured_base_url,
255
342
  target_model=default_model,
256
343
  )
257
344
  )
258
- if model_cfg.get("base_url") and not runtime.get("base_url"):
259
- runtime["base_url"] = model_cfg.get("base_url")
260
- toolsets = list(_get_platform_tools(cfg, "cli"))
345
+ if configured_base_url and not runtime.get("base_url"):
346
+ runtime["base_url"] = configured_base_url
347
+ toolsets = resolve_toolsets(cfg)
261
348
 
262
349
  history = read_session_history(session_id, session_db)
263
350
  return {
@@ -412,6 +499,16 @@ def main() -> int:
412
499
  task_id=agent.session_id,
413
500
  persist_user_message=agent_inputs["user_message"],
414
501
  )
502
+ if not isinstance(result, dict):
503
+ emit(
504
+ {
505
+ "type": "error",
506
+ "message": "Hermes agent returned no result."
507
+ if result is None
508
+ else f"Hermes agent returned invalid result: {type(result).__name__}.",
509
+ }
510
+ )
511
+ return 1
415
512
  final_text = extract_final_text(result, accumulated_text)
416
513
  if not final_text.strip():
417
514
  error_text = extract_error_text(result)