@13rac1/openclaw-plugin-claude-code 1.0.3

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,108 @@
1
+ export interface PodmanConfig {
2
+ runtime: string;
3
+ image: string;
4
+ startupTimeout: number;
5
+ idleTimeout: number;
6
+ memory: string;
7
+ cpus: string;
8
+ network: string;
9
+ apparmorProfile?: string;
10
+ maxOutputSize: number;
11
+ }
12
+ export type ErrorType = "startup_timeout" | "idle_timeout" | "oom" | "crash" | "spawn_failed";
13
+ export interface ResourceMetrics {
14
+ memoryUsageMB?: number;
15
+ memoryLimitMB?: number;
16
+ memoryPercent?: number;
17
+ cpuPercent?: number;
18
+ }
19
+ export interface ClaudeCodeResult {
20
+ content: string;
21
+ sessionId: string | null;
22
+ exitCode: number;
23
+ elapsedSeconds?: number;
24
+ errorType?: ErrorType;
25
+ outputTruncated?: boolean;
26
+ originalSize?: number;
27
+ metrics?: ResourceMetrics;
28
+ }
29
+ /** Container status returned by getContainerStatus */
30
+ export interface ContainerStatus {
31
+ running: boolean;
32
+ exitCode: number | null;
33
+ startedAt: string | null;
34
+ finishedAt: string | null;
35
+ }
36
+ /** Container info returned by listContainersByPrefix */
37
+ export interface ContainerInfo {
38
+ name: string;
39
+ running: boolean;
40
+ createdAt: string;
41
+ }
42
+ /** Result of starting a detached container */
43
+ export interface DetachedStartResult {
44
+ containerName: string;
45
+ containerId: string;
46
+ }
47
+ export declare class PodmanRunner {
48
+ private config;
49
+ constructor(config: PodmanConfig);
50
+ runClaudeCode(params: {
51
+ sessionKey: string;
52
+ prompt: string;
53
+ claudeDir: string;
54
+ workspaceDir: string;
55
+ resumeSessionId?: string;
56
+ apiKey?: string;
57
+ }): Promise<ClaudeCodeResult>;
58
+ private buildArgs;
59
+ private execute;
60
+ private parseOutput;
61
+ checkImage(): Promise<boolean>;
62
+ killContainer(sessionKey: string): Promise<void>;
63
+ verifyContainerRunning(containerName: string, retries?: number): Promise<boolean>;
64
+ /**
65
+ * Get resource metrics for a running container.
66
+ * Returns undefined if container is not running or stats unavailable.
67
+ */
68
+ getContainerStats(containerName: string): Promise<ResourceMetrics | undefined>;
69
+ /**
70
+ * Parse memory string like "123.4MiB" or "1.2GiB" to MB
71
+ */
72
+ parseMemoryString(memStr: string | undefined): number | undefined;
73
+ /**
74
+ * Start a container in detached mode. Returns immediately with container ID.
75
+ */
76
+ startDetached(params: {
77
+ sessionKey: string;
78
+ prompt: string;
79
+ claudeDir: string;
80
+ workspaceDir: string;
81
+ resumeSessionId?: string;
82
+ apiKey?: string;
83
+ }): Promise<DetachedStartResult>;
84
+ /**
85
+ * Get the status of a container by name.
86
+ */
87
+ getContainerStatus(containerName: string): Promise<ContainerStatus | null>;
88
+ /**
89
+ * Get logs from a container.
90
+ */
91
+ getContainerLogs(containerName: string, opts?: {
92
+ since?: string;
93
+ tail?: number;
94
+ }): Promise<string | null>;
95
+ /**
96
+ * List all containers matching a name prefix.
97
+ */
98
+ listContainersByPrefix(prefix: string): Promise<ContainerInfo[]>;
99
+ /**
100
+ * Generate container name from session key.
101
+ */
102
+ containerNameFromSessionKey(sessionKey: string): string;
103
+ /**
104
+ * Extract session key from container name.
105
+ */
106
+ sessionKeyFromContainerName(containerName: string): string | null;
107
+ }
108
+ //# sourceMappingURL=podman-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"podman-runner.d.ts","sourceRoot":"","sources":["../src/podman-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,537 @@
1
+ import { spawn } from "node:child_process";
2
+ function isClaudeCodeOutput(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ function isPodmanStatsOutput(value) {
6
+ return typeof value === "object" && value !== null;
7
+ }
8
+ function isPodmanInspectOutput(value) {
9
+ return typeof value === "object" && value !== null;
10
+ }
11
+ function isPodmanPsOutput(value) {
12
+ return typeof value === "object" && value !== null;
13
+ }
14
+ export class PodmanRunner {
15
+ config;
16
+ constructor(config) {
17
+ this.config = config;
18
+ }
19
+ async runClaudeCode(params) {
20
+ const containerName = `claude-${params.sessionKey.replace(/[^a-zA-Z0-9-]/g, "-")}`;
21
+ // Clean up stale container from previous crash
22
+ await this.killContainer(params.sessionKey);
23
+ const args = this.buildArgs(params, containerName);
24
+ return this.execute(args, containerName);
25
+ }
26
+ buildArgs(params, containerName) {
27
+ // Build the claude command to run inside bash
28
+ // Claude doesn't output when run as PID 1, must run through bash
29
+ const resumeFlag = params.resumeSessionId ? `--resume '${params.resumeSessionId}'` : "";
30
+ const escapedPrompt = params.prompt.replace(/'/g, "'\\''");
31
+ const claudeCmd = `claude --print --dangerously-skip-permissions ${resumeFlag} -p '${escapedPrompt}' < /dev/null 2>&1`;
32
+ const args = [
33
+ "run",
34
+ "--rm",
35
+ "--name",
36
+ containerName,
37
+ "--network",
38
+ this.config.network,
39
+ "--cap-drop",
40
+ "ALL",
41
+ ];
42
+ // Add AppArmor profile if configured
43
+ if (this.config.apparmorProfile) {
44
+ args.push("--security-opt", `apparmor=${this.config.apparmorProfile}`);
45
+ }
46
+ // Note: no --read-only, claude needs to write temp files
47
+ args.push("--memory", this.config.memory, "--cpus", this.config.cpus, "--pids-limit", "100", "--tmpfs", "/tmp:rw,noexec,nosuid,size=64m",
48
+ // :U flag handles UID mapping for permissions
49
+ "-v", `${params.claudeDir}:/home/claude/.claude:U`, "-v", `${params.workspaceDir}:/workspace:U`);
50
+ // Only add API key if provided (otherwise uses credentials file)
51
+ if (params.apiKey) {
52
+ args.push("-e", `ANTHROPIC_API_KEY=${params.apiKey}`);
53
+ }
54
+ args.push("-w", "/workspace", "--entrypoint", "/bin/bash", this.config.image, "-c", claudeCmd);
55
+ return args;
56
+ }
57
+ execute(args, containerName) {
58
+ return new Promise((resolve, reject) => {
59
+ const startTime = Date.now();
60
+ const proc = spawn(this.config.runtime, args, {
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ });
63
+ let stdout = "";
64
+ let stderr = "";
65
+ let killed = false;
66
+ let killReason = null;
67
+ let lastActivity = Date.now();
68
+ let hadOutput = false;
69
+ let totalOutputSize = 0;
70
+ let outputTruncated = false;
71
+ const maxSize = this.config.maxOutputSize;
72
+ let lastMetrics;
73
+ const cleanup = () => {
74
+ clearTimeout(startupTimeoutId);
75
+ clearInterval(idleCheckInterval);
76
+ clearInterval(metricsInterval);
77
+ };
78
+ const killProcess = (reason) => {
79
+ if (killed)
80
+ return;
81
+ killed = true;
82
+ killReason = reason;
83
+ cleanup();
84
+ proc.kill("SIGTERM");
85
+ // Give 5s for graceful shutdown, then SIGKILL
86
+ setTimeout(() => {
87
+ if (!proc.killed) {
88
+ proc.kill("SIGKILL");
89
+ }
90
+ }, 5000);
91
+ };
92
+ // Startup timeout - must produce first output within N seconds
93
+ const startupTimeoutId = setTimeout(() => {
94
+ if (!hadOutput) {
95
+ killProcess("startup_timeout");
96
+ }
97
+ }, this.config.startupTimeout * 1000);
98
+ // Idle timeout check - no output for N seconds = hung
99
+ const idleCheckInterval = setInterval(() => {
100
+ if (!hadOutput)
101
+ return; // Still waiting for startup, don't check idle
102
+ const idleSeconds = (Date.now() - lastActivity) / 1000;
103
+ if (idleSeconds > this.config.idleTimeout) {
104
+ killProcess("idle_timeout");
105
+ }
106
+ }, 5000);
107
+ // Periodically sample container metrics (every 10s)
108
+ const metricsInterval = setInterval(() => {
109
+ if (!hadOutput)
110
+ return; // Container not started yet
111
+ void this.getContainerStats(containerName).then((metrics) => {
112
+ if (metrics) {
113
+ lastMetrics = metrics;
114
+ }
115
+ });
116
+ }, 10000);
117
+ const onOutput = () => {
118
+ if (!hadOutput) {
119
+ hadOutput = true;
120
+ clearTimeout(startupTimeoutId);
121
+ }
122
+ lastActivity = Date.now();
123
+ };
124
+ proc.stdout.on("data", (data) => {
125
+ onOutput();
126
+ const dataStr = data.toString();
127
+ if (maxSize > 0 && totalOutputSize + dataStr.length > maxSize) {
128
+ if (!outputTruncated) {
129
+ const remaining = maxSize - totalOutputSize;
130
+ stdout += dataStr.slice(0, remaining);
131
+ outputTruncated = true;
132
+ }
133
+ totalOutputSize += dataStr.length;
134
+ }
135
+ else {
136
+ stdout += dataStr;
137
+ totalOutputSize += dataStr.length;
138
+ }
139
+ });
140
+ proc.stderr.on("data", (data) => {
141
+ onOutput();
142
+ const dataStr = data.toString();
143
+ if (maxSize > 0 && totalOutputSize + dataStr.length > maxSize) {
144
+ if (!outputTruncated) {
145
+ const remaining = maxSize - totalOutputSize;
146
+ stderr += dataStr.slice(0, remaining);
147
+ outputTruncated = true;
148
+ }
149
+ totalOutputSize += dataStr.length;
150
+ }
151
+ else {
152
+ stderr += dataStr;
153
+ totalOutputSize += dataStr.length;
154
+ }
155
+ });
156
+ proc.on("error", (err) => {
157
+ cleanup();
158
+ const elapsed = (Date.now() - startTime) / 1000;
159
+ reject(Object.assign(new Error(`Failed to spawn ${this.config.runtime}: ${err.message}`), {
160
+ errorType: "spawn_failed",
161
+ elapsedSeconds: elapsed,
162
+ }));
163
+ });
164
+ proc.on("close", (code, signal) => {
165
+ cleanup();
166
+ const elapsed = (Date.now() - startTime) / 1000;
167
+ if (killed && killReason) {
168
+ const message = killReason === "startup_timeout"
169
+ ? `Container startup timeout (no output for ${String(this.config.startupTimeout)}s)`
170
+ : `Container idle timeout (no output for ${String(this.config.idleTimeout)}s) after ${elapsed.toFixed(1)}s total`;
171
+ reject(Object.assign(new Error(message), {
172
+ errorType: killReason,
173
+ elapsedSeconds: elapsed,
174
+ }));
175
+ return;
176
+ }
177
+ if (signal === "SIGKILL" || code === 137) {
178
+ reject(Object.assign(new Error(`Container killed (OOM or resource limit) after ${elapsed.toFixed(1)}s: ${stderr || stdout}`), { errorType: "oom", elapsedSeconds: elapsed }));
179
+ return;
180
+ }
181
+ if (code !== 0) {
182
+ reject(Object.assign(new Error(`Container failed (exit ${String(code)}) after ${elapsed.toFixed(1)}s: ${stderr || stdout}`), { errorType: "crash", elapsedSeconds: elapsed }));
183
+ return;
184
+ }
185
+ const result = this.parseOutput(stdout, code);
186
+ result.elapsedSeconds = elapsed;
187
+ if (outputTruncated) {
188
+ result.outputTruncated = true;
189
+ result.originalSize = totalOutputSize;
190
+ }
191
+ if (lastMetrics) {
192
+ result.metrics = lastMetrics;
193
+ }
194
+ resolve(result);
195
+ });
196
+ });
197
+ }
198
+ parseOutput(output, exitCode) {
199
+ // Claude Code --print outputs JSON when available
200
+ // Try to parse as JSON first
201
+ try {
202
+ const lines = output.trim().split("\n");
203
+ for (let i = lines.length - 1; i >= 0; i--) {
204
+ const line = lines[i].trim();
205
+ if (line.startsWith("{") && line.endsWith("}")) {
206
+ const parsed = JSON.parse(line);
207
+ if (isClaudeCodeOutput(parsed)) {
208
+ return {
209
+ content: parsed.result ?? parsed.content ?? parsed.message ?? output,
210
+ sessionId: parsed.session_id ?? parsed.sessionId ?? null,
211
+ exitCode,
212
+ };
213
+ }
214
+ }
215
+ }
216
+ }
217
+ catch {
218
+ // Not JSON, use raw output
219
+ }
220
+ return {
221
+ content: output.trim(),
222
+ sessionId: null,
223
+ exitCode,
224
+ };
225
+ }
226
+ async checkImage() {
227
+ return new Promise((resolve) => {
228
+ const proc = spawn(this.config.runtime, ["image", "exists", this.config.image], {
229
+ stdio: "ignore",
230
+ });
231
+ proc.on("error", () => resolve(false));
232
+ proc.on("close", (code) => resolve(code === 0));
233
+ });
234
+ }
235
+ async killContainer(sessionKey) {
236
+ const containerName = `claude-${sessionKey.replace(/[^a-zA-Z0-9-]/g, "-")}`;
237
+ // First try to kill, then remove (in case it's stopped but not removed)
238
+ await new Promise((resolve) => {
239
+ const proc = spawn(this.config.runtime, ["kill", containerName], {
240
+ stdio: "ignore",
241
+ });
242
+ proc.on("error", () => resolve());
243
+ proc.on("close", () => resolve());
244
+ });
245
+ await new Promise((resolve) => {
246
+ const proc = spawn(this.config.runtime, ["rm", "-f", containerName], {
247
+ stdio: "ignore",
248
+ });
249
+ proc.on("error", () => resolve());
250
+ proc.on("close", () => resolve());
251
+ });
252
+ }
253
+ async verifyContainerRunning(containerName, retries = 3) {
254
+ for (let i = 0; i < retries; i++) {
255
+ await new Promise((r) => setTimeout(r, 500));
256
+ const exists = await new Promise((resolve) => {
257
+ const proc = spawn(this.config.runtime, ["container", "exists", containerName], {
258
+ stdio: "ignore",
259
+ });
260
+ proc.on("close", (code) => resolve(code === 0));
261
+ proc.on("error", () => resolve(false));
262
+ });
263
+ if (exists)
264
+ return true;
265
+ }
266
+ return false;
267
+ }
268
+ /**
269
+ * Get resource metrics for a running container.
270
+ * Returns undefined if container is not running or stats unavailable.
271
+ */
272
+ getContainerStats(containerName) {
273
+ return new Promise((resolve) => {
274
+ const proc = spawn(this.config.runtime, ["stats", "--no-stream", "--format", "json", containerName], { stdio: ["ignore", "pipe", "ignore"] });
275
+ let stdout = "";
276
+ let timeoutId = null;
277
+ proc.stdout.on("data", (data) => {
278
+ stdout += data.toString();
279
+ });
280
+ proc.on("error", () => resolve(undefined));
281
+ proc.on("close", (code) => {
282
+ if (timeoutId)
283
+ clearTimeout(timeoutId);
284
+ if (code !== 0) {
285
+ resolve(undefined);
286
+ return;
287
+ }
288
+ try {
289
+ const stats = JSON.parse(stdout);
290
+ // Podman stats JSON format - may be array or single object
291
+ const statArray = Array.isArray(stats) ? stats : [stats];
292
+ const stat = statArray[0];
293
+ if (!isPodmanStatsOutput(stat)) {
294
+ resolve(undefined);
295
+ return;
296
+ }
297
+ const memUsage = this.parseMemoryString(stat.MemUsage ?? stat.mem_usage);
298
+ const memLimit = this.parseMemoryString(stat.MemLimit ?? stat.mem_limit);
299
+ resolve({
300
+ memoryUsageMB: memUsage,
301
+ memoryLimitMB: memLimit,
302
+ memoryPercent: stat.MemPerc
303
+ ? parseFloat(String(stat.MemPerc).replace("%", ""))
304
+ : undefined,
305
+ cpuPercent: stat.CPUPerc
306
+ ? parseFloat(String(stat.CPUPerc).replace("%", ""))
307
+ : undefined,
308
+ });
309
+ }
310
+ catch {
311
+ resolve(undefined);
312
+ }
313
+ });
314
+ // Timeout after 5s
315
+ timeoutId = setTimeout(() => {
316
+ proc.kill();
317
+ resolve(undefined);
318
+ }, 5000);
319
+ });
320
+ }
321
+ /**
322
+ * Parse memory string like "123.4MiB" or "1.2GiB" to MB
323
+ */
324
+ parseMemoryString(memStr) {
325
+ if (!memStr)
326
+ return undefined;
327
+ // Handle "used / limit" format (e.g., "256MiB / 512MiB")
328
+ const parts = memStr.split("/");
329
+ const valueStr = parts[0].trim();
330
+ const match = /^([\d.]+)\s*(B|KB|KiB|MB|MiB|GB|GiB)/i.exec(valueStr);
331
+ if (!match)
332
+ return undefined;
333
+ const value = parseFloat(match[1]);
334
+ const unit = match[2].toLowerCase();
335
+ switch (unit) {
336
+ case "b":
337
+ return value / (1024 * 1024);
338
+ case "kb":
339
+ case "kib":
340
+ return value / 1024;
341
+ case "mb":
342
+ case "mib":
343
+ return value;
344
+ case "gb":
345
+ case "gib":
346
+ return value * 1024;
347
+ default:
348
+ return undefined;
349
+ }
350
+ }
351
+ /**
352
+ * Start a container in detached mode. Returns immediately with container ID.
353
+ */
354
+ async startDetached(params) {
355
+ const containerName = `claude-${params.sessionKey.replace(/[^a-zA-Z0-9-]/g, "-")}`;
356
+ // Clean up stale container from previous run
357
+ await this.killContainer(params.sessionKey);
358
+ // Build the claude command
359
+ const resumeFlag = params.resumeSessionId ? `--resume '${params.resumeSessionId}'` : "";
360
+ const escapedPrompt = params.prompt.replace(/'/g, "'\\''");
361
+ const claudeCmd = `claude --print --dangerously-skip-permissions ${resumeFlag} -p '${escapedPrompt}' < /dev/null 2>&1`;
362
+ const args = [
363
+ "run",
364
+ "--detach",
365
+ "--name",
366
+ containerName,
367
+ "--network",
368
+ this.config.network,
369
+ "--cap-drop",
370
+ "ALL",
371
+ ];
372
+ if (this.config.apparmorProfile) {
373
+ args.push("--security-opt", `apparmor=${this.config.apparmorProfile}`);
374
+ }
375
+ args.push("--memory", this.config.memory, "--cpus", this.config.cpus, "--pids-limit", "100", "--tmpfs", "/tmp:rw,noexec,nosuid,size=64m", "-v", `${params.claudeDir}:/home/claude/.claude:U`, "-v", `${params.workspaceDir}:/workspace:U`);
376
+ if (params.apiKey) {
377
+ args.push("-e", `ANTHROPIC_API_KEY=${params.apiKey}`);
378
+ }
379
+ args.push("-w", "/workspace", "--entrypoint", "/bin/bash", this.config.image, "-c", claudeCmd);
380
+ return new Promise((resolve, reject) => {
381
+ const proc = spawn(this.config.runtime, args, {
382
+ stdio: ["ignore", "pipe", "pipe"],
383
+ });
384
+ let stdout = "";
385
+ let stderr = "";
386
+ proc.stdout.on("data", (data) => {
387
+ stdout += data.toString();
388
+ });
389
+ proc.stderr.on("data", (data) => {
390
+ stderr += data.toString();
391
+ });
392
+ proc.on("error", (err) => {
393
+ reject(new Error(`Failed to spawn ${this.config.runtime}: ${err.message}`));
394
+ });
395
+ proc.on("close", (code) => {
396
+ if (code !== 0) {
397
+ reject(new Error(`Failed to start container: ${stderr || stdout}`));
398
+ return;
399
+ }
400
+ const containerId = stdout.trim();
401
+ resolve({ containerName, containerId });
402
+ });
403
+ });
404
+ }
405
+ /**
406
+ * Get the status of a container by name.
407
+ */
408
+ async getContainerStatus(containerName) {
409
+ return new Promise((resolve) => {
410
+ const proc = spawn(this.config.runtime, ["inspect", "--format", "json", containerName], {
411
+ stdio: ["ignore", "pipe", "ignore"],
412
+ });
413
+ let stdout = "";
414
+ proc.stdout.on("data", (data) => {
415
+ stdout += data.toString();
416
+ });
417
+ proc.on("error", () => resolve(null));
418
+ proc.on("close", (code) => {
419
+ if (code !== 0) {
420
+ resolve(null);
421
+ return;
422
+ }
423
+ try {
424
+ const parsed = JSON.parse(stdout);
425
+ const inspectArray = Array.isArray(parsed) ? parsed : [parsed];
426
+ const inspect = inspectArray[0];
427
+ if (!isPodmanInspectOutput(inspect) || !inspect.State) {
428
+ resolve(null);
429
+ return;
430
+ }
431
+ const state = inspect.State;
432
+ resolve({
433
+ running: state.Running ?? false,
434
+ exitCode: state.ExitCode ?? null,
435
+ startedAt: state.StartedAt ?? null,
436
+ finishedAt: state.FinishedAt ?? null,
437
+ });
438
+ }
439
+ catch {
440
+ resolve(null);
441
+ }
442
+ });
443
+ });
444
+ }
445
+ /**
446
+ * Get logs from a container.
447
+ */
448
+ async getContainerLogs(containerName, opts) {
449
+ return new Promise((resolve) => {
450
+ const args = ["logs"];
451
+ if (opts?.since) {
452
+ args.push("--since", opts.since);
453
+ }
454
+ if (opts?.tail !== undefined) {
455
+ args.push("--tail", String(opts.tail));
456
+ }
457
+ args.push(containerName);
458
+ const proc = spawn(this.config.runtime, args, {
459
+ stdio: ["ignore", "pipe", "pipe"],
460
+ });
461
+ let output = "";
462
+ proc.stdout.on("data", (data) => {
463
+ output += data.toString();
464
+ });
465
+ proc.stderr.on("data", (data) => {
466
+ output += data.toString();
467
+ });
468
+ proc.on("error", () => resolve(null));
469
+ proc.on("close", (code) => {
470
+ if (code !== 0) {
471
+ resolve(null);
472
+ return;
473
+ }
474
+ resolve(output);
475
+ });
476
+ });
477
+ }
478
+ /**
479
+ * List all containers matching a name prefix.
480
+ */
481
+ async listContainersByPrefix(prefix) {
482
+ return new Promise((resolve) => {
483
+ const proc = spawn(this.config.runtime, ["ps", "-a", "--filter", `name=^${prefix}`, "--format", "json"], { stdio: ["ignore", "pipe", "ignore"] });
484
+ let stdout = "";
485
+ proc.stdout.on("data", (data) => {
486
+ stdout += data.toString();
487
+ });
488
+ proc.on("error", () => resolve([]));
489
+ proc.on("close", (code) => {
490
+ if (code !== 0) {
491
+ resolve([]);
492
+ return;
493
+ }
494
+ try {
495
+ // Podman outputs one JSON object per line
496
+ const lines = stdout.trim().split("\n").filter(Boolean);
497
+ const containers = [];
498
+ for (const line of lines) {
499
+ const parsed = JSON.parse(line);
500
+ if (!isPodmanPsOutput(parsed))
501
+ continue;
502
+ const name = Array.isArray(parsed.Names) ? parsed.Names[0] : parsed.Names;
503
+ if (!name)
504
+ continue;
505
+ const running = parsed.State === "running" ||
506
+ (typeof parsed.Status === "string" && parsed.Status.startsWith("Up"));
507
+ containers.push({
508
+ name,
509
+ running,
510
+ createdAt: parsed.Created ?? parsed.CreatedAt ?? "",
511
+ });
512
+ }
513
+ resolve(containers);
514
+ }
515
+ catch {
516
+ resolve([]);
517
+ }
518
+ });
519
+ });
520
+ }
521
+ /**
522
+ * Generate container name from session key.
523
+ */
524
+ containerNameFromSessionKey(sessionKey) {
525
+ return `claude-${sessionKey.replace(/[^a-zA-Z0-9-]/g, "-")}`;
526
+ }
527
+ /**
528
+ * Extract session key from container name.
529
+ */
530
+ sessionKeyFromContainerName(containerName) {
531
+ if (!containerName.startsWith("claude-")) {
532
+ return null;
533
+ }
534
+ return containerName.slice(7); // Remove "claude-" prefix
535
+ }
536
+ }
537
+ //# sourceMappingURL=podman-runner.js.map