palmier 0.2.7 → 0.2.8

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/src/types.ts CHANGED
@@ -1,62 +1,62 @@
1
- export interface HostConfig {
2
- hostId: string;
3
- projectRoot: string;
4
-
5
- // NATS (always enabled)
6
- nats?: boolean;
7
- natsUrl?: string;
8
- natsWsUrl?: string;
9
- natsToken?: string;
10
-
11
- // Detected agent CLIs
12
- agents?: Array<{ key: string; label: string }>;
13
- }
14
-
15
- export interface TaskFrontmatter {
16
- id: string;
17
- name: string;
18
- user_prompt: string;
19
- agent: string;
20
- triggers: Trigger[];
21
- triggers_enabled: boolean;
22
- requires_confirmation: boolean;
23
- permissions?: RequiredPermission[];
24
- command?: string;
25
- }
26
-
27
- export interface Trigger {
28
- type: "cron" | "once";
29
- value: string;
30
- }
31
-
32
- export interface ParsedTask {
33
- frontmatter: TaskFrontmatter;
34
- body: string;
35
- }
36
-
37
- export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
38
-
39
- export interface TaskStatus {
40
- running_state: TaskRunningState;
41
- time_stamp: number;
42
- pending_confirmation?: boolean;
43
- pending_permission?: RequiredPermission[];
44
- pending_input?: string[];
45
- user_input?: string[];
46
- }
47
-
48
- export interface HistoryEntry {
49
- task_id: string;
50
- result_file: string;
51
- }
52
-
53
- export interface RequiredPermission {
54
- name: string;
55
- description: string;
56
- }
57
-
58
- export interface RpcMessage {
59
- method: string;
60
- params: Record<string, unknown>;
61
- sessionToken?: string;
62
- }
1
+ export interface HostConfig {
2
+ hostId: string;
3
+ projectRoot: string;
4
+
5
+ // NATS (always enabled)
6
+ nats?: boolean;
7
+ natsUrl?: string;
8
+ natsWsUrl?: string;
9
+ natsToken?: string;
10
+
11
+ // Detected agent CLIs
12
+ agents?: Array<{ key: string; label: string }>;
13
+ }
14
+
15
+ export interface TaskFrontmatter {
16
+ id: string;
17
+ name: string;
18
+ user_prompt: string;
19
+ agent: string;
20
+ triggers: Trigger[];
21
+ triggers_enabled: boolean;
22
+ requires_confirmation: boolean;
23
+ permissions?: RequiredPermission[];
24
+ command?: string;
25
+ }
26
+
27
+ export interface Trigger {
28
+ type: "cron" | "once";
29
+ value: string;
30
+ }
31
+
32
+ export interface ParsedTask {
33
+ frontmatter: TaskFrontmatter;
34
+ body: string;
35
+ }
36
+
37
+ export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
38
+
39
+ export interface TaskStatus {
40
+ running_state: TaskRunningState;
41
+ time_stamp: number;
42
+ pending_confirmation?: boolean;
43
+ pending_permission?: RequiredPermission[];
44
+ pending_input?: string[];
45
+ user_input?: string[];
46
+ }
47
+
48
+ export interface HistoryEntry {
49
+ task_id: string;
50
+ result_file: string;
51
+ }
52
+
53
+ export interface RequiredPermission {
54
+ name: string;
55
+ description: string;
56
+ }
57
+
58
+ export interface RpcMessage {
59
+ method: string;
60
+ params: Record<string, unknown>;
61
+ sessionToken?: string;
62
+ }
@@ -1,7 +0,0 @@
1
- /**
2
- * Handle a Claude Code hook invocation.
3
- * Called by Claude Code as a subprocess. Reads hook event from stdin,
4
- * dispatches by hook_name, and outputs response to stdout.
5
- */
6
- export declare function hookCommand(): Promise<void>;
7
- //# sourceMappingURL=hook.d.ts.map
@@ -1,208 +0,0 @@
1
- import { v4 as uuidv4 } from "uuid";
2
- import { StringCodec } from "nats";
3
- import { appendFileSync } from "fs";
4
- import { loadConfig } from "../config.js";
5
- import { connectNats } from "../nats-client.js";
6
- function log(msg) {
7
- const line = `[${new Date().toISOString()}] ${msg}\n`;
8
- appendFileSync("/tmp/palmier-hook.log", line);
9
- }
10
- /**
11
- * Handle a Claude Code hook invocation.
12
- * Called by Claude Code as a subprocess. Reads hook event from stdin,
13
- * dispatches by hook_name, and outputs response to stdout.
14
- */
15
- export async function hookCommand() {
16
- const rawInput = await readStdin();
17
- let event;
18
- try {
19
- event = JSON.parse(rawInput);
20
- }
21
- catch {
22
- console.error("Failed to parse hook event from stdin");
23
- process.exit(1);
24
- }
25
- log(`received: ${JSON.stringify(event).slice(0, 500)}`);
26
- const taskId = process.env.PALMIER_TASK_ID;
27
- if (!taskId) {
28
- log("no PALMIER_TASK_ID, exiting");
29
- return;
30
- }
31
- const config = loadConfig();
32
- const nc = await connectNats(config);
33
- const sc = StringCodec();
34
- try {
35
- const js = nc.jetstream();
36
- const kv = await js.views.kv("pending-hooks");
37
- switch (event.hook_event_name) {
38
- case "PermissionRequest":
39
- await handlePermissionRequest(config, nc, kv, sc, event, taskId);
40
- break;
41
- case "Notification":
42
- await handleNotification(config, nc, kv, sc, event, taskId);
43
- break;
44
- case "Stop":
45
- await handleStop(config, nc, sc, taskId);
46
- break;
47
- default:
48
- // Unknown hook, exit silently
49
- break;
50
- }
51
- }
52
- finally {
53
- await nc.drain();
54
- }
55
- }
56
- function permissionResponse(behavior) {
57
- return {
58
- hookSpecificOutput: {
59
- hookEventName: "PermissionRequest",
60
- decision: { behavior },
61
- },
62
- };
63
- }
64
- async function handlePermissionRequest(config, nc, kv, sc, event, taskId) {
65
- const hookId = uuidv4();
66
- const kvKey = `${config.agentId}.${taskId}.${hookId}`;
67
- // Start watching BEFORE writing
68
- const watch = await kv.watch({ key: kvKey });
69
- // Write hook payload to KV
70
- const payload = {
71
- type: "permission",
72
- task_id: taskId,
73
- hook_id: hookId,
74
- agent_id: config.agentId,
75
- user_id: config.userId,
76
- details: {
77
- tool: event.tool_name,
78
- input: event.tool_input,
79
- },
80
- status: "pending",
81
- };
82
- log(`permission: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
83
- await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
84
- // Publish push notification
85
- nc.publish(`user.${config.userId}.push.request.permission`, sc.encode(JSON.stringify({
86
- type: "permission",
87
- task_id: taskId,
88
- hook_id: hookId,
89
- agent_id: config.agentId,
90
- tool: event.tool_name,
91
- input: event.tool_input,
92
- })));
93
- // Wait for status change
94
- for await (const entry of watch) {
95
- log(`permission: watch event op=${entry.operation} key=${entry.key}`);
96
- if (entry.operation === "DEL" || entry.operation === "PURGE") {
97
- // Key deleted, deny by default
98
- log(`permission: key deleted/purged, denying`);
99
- process.stdout.write(JSON.stringify(permissionResponse("deny")));
100
- return;
101
- }
102
- try {
103
- const updated = JSON.parse(sc.decode(entry.value));
104
- log(`permission: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
105
- if (updated.status === "confirmed" || updated.status === "allowed") {
106
- const out = JSON.stringify(permissionResponse("allow"));
107
- log(`permission: allowing, stdout=${out}`);
108
- process.stdout.write(out);
109
- await kv.delete(kvKey);
110
- return;
111
- }
112
- else if (updated.status === "denied" || updated.status === "aborted") {
113
- const out = JSON.stringify(permissionResponse("deny"));
114
- log(`permission: denying, stdout=${out}`);
115
- process.stdout.write(out);
116
- await kv.delete(kvKey);
117
- return;
118
- }
119
- // Still pending, keep watching
120
- }
121
- catch {
122
- // Couldn't parse, keep watching
123
- }
124
- }
125
- }
126
- async function handleNotification(config, nc, kv, sc, event, taskId) {
127
- const message = event.message || "";
128
- // Check if notification requires user input
129
- // Look for patterns suggesting input is needed
130
- const inputPatterns = [
131
- /\bwait(ing)?\s+(for|on)\s+(user\s+)?input\b/i,
132
- /\bplease\s+(provide|enter|type|input)\b/i,
133
- /\buser\s+input\s+(required|needed)\b/i,
134
- /\bask(ing)?\s+(the\s+)?user\b/i,
135
- /\brequires?\s+(user\s+)?input\b/i,
136
- /\bprompt(ing)?\s+(the\s+)?user\b/i,
137
- ];
138
- const needsInput = inputPatterns.some((pattern) => pattern.test(message));
139
- if (!needsInput) {
140
- // No input needed, exit silently
141
- return;
142
- }
143
- const hookId = uuidv4();
144
- const kvKey = `${config.agentId}.${taskId}.${hookId}`;
145
- // Start watching BEFORE writing
146
- const watch = await kv.watch({ key: kvKey });
147
- // Write hook payload to KV
148
- const payload = {
149
- type: "input",
150
- task_id: taskId,
151
- hook_id: hookId,
152
- agent_id: config.agentId,
153
- user_id: config.userId,
154
- details: {
155
- message: event.message,
156
- },
157
- status: "pending",
158
- };
159
- log(`input: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
160
- await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
161
- // Publish push notification
162
- nc.publish(`user.${config.userId}.push.notify.input_needed`, sc.encode(JSON.stringify({
163
- type: "input",
164
- task_id: taskId,
165
- hook_id: hookId,
166
- agent_id: config.agentId,
167
- message: event.message,
168
- })));
169
- // Wait for status change - the status field will contain the user's input text
170
- for await (const entry of watch) {
171
- log(`input: watch event op=${entry.operation} key=${entry.key}`);
172
- if (entry.operation === "DEL" || entry.operation === "PURGE") {
173
- log(`input: key deleted/purged`);
174
- return;
175
- }
176
- try {
177
- const updated = JSON.parse(sc.decode(entry.value));
178
- log(`input: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
179
- if (updated.status !== "pending") {
180
- // The status field contains the user's input text
181
- log(`input: resolved with user input`);
182
- process.stdout.write(updated.status);
183
- await kv.delete(kvKey);
184
- return;
185
- }
186
- // Still pending, keep watching
187
- }
188
- catch {
189
- // Couldn't parse, keep watching
190
- }
191
- }
192
- }
193
- async function handleStop(config, nc, sc, taskId) {
194
- // Publish completion notification
195
- nc.publish(`user.${config.userId}.push.notify.complete`, sc.encode(JSON.stringify({
196
- type: "complete",
197
- task_id: taskId,
198
- agent_id: config.agentId,
199
- })));
200
- }
201
- async function readStdin() {
202
- const chunks = [];
203
- for await (const chunk of process.stdin) {
204
- chunks.push(chunk);
205
- }
206
- return Buffer.concat(chunks).toString("utf-8");
207
- }
208
- //# sourceMappingURL=hook.js.map
@@ -1,14 +0,0 @@
1
- /**
2
- * Post-exit cleanup for a task process.
3
- *
4
- * Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
5
- * after the main `palmier run <taskId>` process exits.
6
- *
7
- * - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
8
- * - If status.json shows "abort", the RPC handler already wrote status and broadcast —
9
- * just write the RESULT file and append history.
10
- * - If status.json shows "start", the process died unexpectedly — write "fail" status,
11
- * RESULT file, append history, and broadcast event.
12
- */
13
- export declare function taskCleanupCommand(taskId: string): Promise<void>;
14
- //# sourceMappingURL=task-cleanup.d.ts.map
@@ -1,84 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { loadConfig } from "../config.js";
4
- import { connectNats } from "../nats-client.js";
5
- import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
6
- import { publishHostEvent } from "../events.js";
7
- /**
8
- * Write a minimal RESULT file for a task that exited without writing one itself.
9
- * Uses the status.json time_stamp as the start time.
10
- */
11
- function writeCleanupResult(taskDir, taskName, runningState, startTime) {
12
- const endTime = Date.now();
13
- const resultFileName = `RESULT-${endTime}.md`;
14
- // Find the task snapshot file that matches the start time
15
- const taskSnapshotName = `TASK-${startTime}.md`;
16
- const taskFile = fs.existsSync(path.join(taskDir, taskSnapshotName)) ? taskSnapshotName : "";
17
- const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskFile}\n---\nTask process exited unexpectedly.`;
18
- fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
19
- return resultFileName;
20
- }
21
- /**
22
- * Post-exit cleanup for a task process.
23
- *
24
- * Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
25
- * after the main `palmier run <taskId>` process exits.
26
- *
27
- * - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
28
- * - If status.json shows "abort", the RPC handler already wrote status and broadcast —
29
- * just write the RESULT file and append history.
30
- * - If status.json shows "start", the process died unexpectedly — write "fail" status,
31
- * RESULT file, append history, and broadcast event.
32
- */
33
- export async function taskCleanupCommand(taskId) {
34
- const config = loadConfig();
35
- const taskDir = getTaskDir(config.projectRoot, taskId);
36
- const status = readTaskStatus(taskDir);
37
- if (!status) {
38
- console.log(`[task-cleanup] No status.json for task ${taskId}, nothing to do.`);
39
- return;
40
- }
41
- // Process already handled its own cleanup
42
- if (status.running_state === "finish" || status.running_state === "fail") {
43
- console.log(`[task-cleanup] Task ${taskId} already in terminal state: ${status.running_state}`);
44
- return;
45
- }
46
- // Read task name for RESULT file
47
- let taskName = taskId;
48
- try {
49
- const task = parseTaskFile(taskDir);
50
- taskName = task.frontmatter.name || taskId;
51
- }
52
- catch { /* use taskId as fallback name */ }
53
- const startTime = status.time_stamp;
54
- if (status.running_state === "abort") {
55
- // RPC handler already wrote status and broadcast — just write RESULT + history
56
- console.log(`[task-cleanup] Task ${taskId} was aborted via RPC, writing RESULT.`);
57
- const resultFileName = writeCleanupResult(taskDir, taskName, "abort", startTime);
58
- appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
59
- return;
60
- }
61
- // status.running_state === "start" — unexpected death
62
- console.log(`[task-cleanup] Task ${taskId} died unexpectedly, marking as failed.`);
63
- writeTaskStatus(taskDir, { running_state: "fail", time_stamp: Date.now() });
64
- const resultFileName = writeCleanupResult(taskDir, taskName, "fail", startTime);
65
- appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
66
- // Broadcast failure event via NATS and/or HTTP, consistent with other status pushes
67
- const mode = config.mode ?? "nats";
68
- const useNats = mode === "nats" || mode === "auto";
69
- const useHttp = mode === "lan" || mode === "auto";
70
- let nc;
71
- try {
72
- if (useNats) {
73
- nc = await connectNats(config);
74
- }
75
- const payload = { event_type: "running-state", running_state: "fail", name: taskName };
76
- await publishHostEvent(nc, config, taskId, payload, useHttp);
77
- }
78
- finally {
79
- if (nc && !nc.isClosed()) {
80
- await nc.drain();
81
- }
82
- }
83
- }
84
- //# sourceMappingURL=task-cleanup.js.map
@@ -1,28 +0,0 @@
1
- You are a planning assistant for a personal computer AI agent. Given a task description, produce a detailed Markdown execution plan that the agent can later follow step by step. **Do not execute any part of the task yourself.**
2
-
3
- The plan must include the following sections:
4
-
5
- ### 1. Goal
6
- What the task accomplishes and the expected end state.
7
-
8
- ### 2. Prerequisites
9
- What must be true before starting:
10
- - Required software and versions
11
- - Files or data that must be present
12
- - Permissions or access needed
13
- - Environment state (e.g., running services, network access)
14
-
15
- ### 3. Plan
16
- A numbered sequence of concrete, actionable steps to complete the task.
17
- Use sub-steps for complex actions. Include conditional branches where behavior may vary (e.g., "If file exists, do A; otherwise, do B"). Each step should be specific enough that the agent can execute it without ambiguity.
18
-
19
- ### 4. Edge Cases & Risks
20
- Anything that could go wrong and how to handle it:
21
- - Common failure modes
22
- - Platform-specific differences
23
- - Race conditions or timing issues
24
- - Data loss risks and mitigation
25
-
26
- Format the entire document as Markdown with proper headings, code blocks for commands, and tables where appropriate.
27
-
28
- **Task description:**
package/dist/systemd.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { HostConfig } from "./types.js";
2
- import type { ParsedTask } from "./types.js";
3
- /**
4
- * Convert a cron expression (5-field) to a systemd OnCalendar string.
5
- * Handles basic cron patterns: minute hour day-of-month month day-of-week
6
- */
7
- export declare function cronToOnCalendar(cron: string): string;
8
- /**
9
- * Install a systemd user timer + service for a task.
10
- */
11
- export declare function installTaskTimer(config: HostConfig, task: ParsedTask): void;
12
- /**
13
- * Remove a task's systemd timer and service files.
14
- */
15
- export declare function removeTaskTimer(taskId: string): void;
16
- /**
17
- * Run systemctl --user daemon-reload.
18
- */
19
- export declare function daemonReload(): void;
20
- //# sourceMappingURL=systemd.d.ts.map
package/dist/systemd.js DELETED
@@ -1,145 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { homedir } from "os";
4
- import { execSync } from "child_process";
5
- const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
6
- function getTimerName(taskId) {
7
- return `palmier-task-${taskId}.timer`;
8
- }
9
- function getServiceName(taskId) {
10
- return `palmier-task-${taskId}.service`;
11
- }
12
- /**
13
- * Convert a cron expression (5-field) to a systemd OnCalendar string.
14
- * Handles basic cron patterns: minute hour day-of-month month day-of-week
15
- */
16
- export function cronToOnCalendar(cron) {
17
- const parts = cron.trim().split(/\s+/);
18
- if (parts.length !== 5) {
19
- throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
20
- }
21
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
22
- // Map cron day-of-week names/numbers to systemd abbreviated names
23
- const dowMap = {
24
- "0": "Sun",
25
- "1": "Mon",
26
- "2": "Tue",
27
- "3": "Wed",
28
- "4": "Thu",
29
- "5": "Fri",
30
- "6": "Sat",
31
- "7": "Sun",
32
- };
33
- // Convert day-of-week
34
- let dow = dayOfWeek === "*" ? "*" : dayOfWeek;
35
- if (dowMap[dow]) {
36
- dow = dowMap[dow];
37
- }
38
- // Build OnCalendar string
39
- // Format: DayOfWeek Year-Month-Day Hour:Minute:Second
40
- const monthPart = month === "*" ? "*" : month.padStart(2, "0");
41
- const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
42
- const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
43
- const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
44
- if (dow === "*") {
45
- return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
46
- }
47
- return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
48
- }
49
- /**
50
- * Install a systemd user timer + service for a task.
51
- */
52
- export function installTaskTimer(config, task) {
53
- fs.mkdirSync(UNIT_DIR, { recursive: true });
54
- const taskId = task.frontmatter.id;
55
- const serviceName = getServiceName(taskId);
56
- const timerName = getTimerName(taskId);
57
- // Determine the palmier binary path
58
- const palmierBin = process.argv[1] || "palmier";
59
- // Generate service unit
60
- const serviceContent = `[Unit]
61
- Description=Palmier Task: ${taskId}
62
-
63
- [Service]
64
- Type=oneshot
65
- ExecStart=${palmierBin} run ${taskId}
66
- WorkingDirectory=${config.projectRoot}
67
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
68
- `;
69
- // Write service unit (always needed for on-demand runs)
70
- fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
71
- daemonReload();
72
- // Only create and enable a timer if there are actual triggers
73
- const triggers = task.frontmatter.triggers || [];
74
- const onCalendarLines = [];
75
- for (const trigger of triggers) {
76
- if (trigger.type === "cron") {
77
- onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
78
- }
79
- else if (trigger.type === "once") {
80
- onCalendarLines.push(`OnActiveSec=${trigger.value}`);
81
- }
82
- }
83
- if (onCalendarLines.length > 0) {
84
- const timerContent = `[Unit]
85
- Description=Timer for Palmier Task: ${taskId}
86
-
87
- [Timer]
88
- ${onCalendarLines.join("\n")}
89
- Persistent=true
90
-
91
- [Install]
92
- WantedBy=timers.target
93
- `;
94
- fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
95
- daemonReload();
96
- try {
97
- execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
98
- }
99
- catch (err) {
100
- const e = err;
101
- console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
102
- }
103
- }
104
- }
105
- /**
106
- * Remove a task's systemd timer and service files.
107
- */
108
- export function removeTaskTimer(taskId) {
109
- const timerName = getTimerName(taskId);
110
- const serviceName = getServiceName(taskId);
111
- const timerPath = path.join(UNIT_DIR, timerName);
112
- const servicePath = path.join(UNIT_DIR, serviceName);
113
- // Only stop/disable the timer if the file exists
114
- if (fs.existsSync(timerPath)) {
115
- try {
116
- execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" });
117
- }
118
- catch {
119
- // Timer might not be running
120
- }
121
- try {
122
- execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
123
- }
124
- catch {
125
- // Timer might not be enabled
126
- }
127
- fs.unlinkSync(timerPath);
128
- }
129
- if (fs.existsSync(servicePath))
130
- fs.unlinkSync(servicePath);
131
- daemonReload();
132
- }
133
- /**
134
- * Run systemctl --user daemon-reload.
135
- */
136
- export function daemonReload() {
137
- try {
138
- execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
139
- }
140
- catch (err) {
141
- const e = err;
142
- console.error(`daemon-reload failed: ${e.stderr || err}`);
143
- }
144
- }
145
- //# sourceMappingURL=systemd.js.map