palmier 0.2.5 → 0.2.7
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/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +286 -219
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +12 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +12 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +13 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +59 -95
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +12 -27
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +311 -124
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +77 -17
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +3 -17
- package/dist/events.d.ts +9 -0
- package/dist/events.js +46 -0
- package/dist/index.js +15 -0
- package/dist/platform/linux.d.ts +2 -0
- package/dist/platform/linux.js +22 -1
- package/dist/platform/platform.d.ts +4 -0
- package/dist/platform/windows.d.ts +3 -0
- package/dist/platform/windows.js +99 -82
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +43 -52
- package/dist/spawn-command.d.ts +29 -6
- package/dist/spawn-command.js +38 -15
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +5 -5
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -43
- package/src/agents/codex.ts +11 -12
- package/src/agents/gemini.ts +12 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +62 -119
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +12 -31
- package/src/commands/pair.ts +50 -63
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +375 -143
- package/src/commands/serve.ts +96 -17
- package/src/config.ts +3 -18
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +51 -0
- package/src/index.ts +17 -0
- package/src/platform/linux.ts +25 -1
- package/src/platform/platform.ts +6 -0
- package/src/platform/windows.ts +100 -89
- package/src/rpc-handler.ts +46 -55
- package/src/spawn-command.ts +120 -83
- package/src/transports/http-transport.ts +123 -19
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +6 -8
package/dist/platform/windows.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
|
-
const TASK_PREFIX = "PalmierTask-";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
5
|
+
import { CONFIG_DIR } from "../config.js";
|
|
6
|
+
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
9
7
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
10
|
-
const
|
|
8
|
+
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Build the /tr value for schtasks: a single string with quoted paths
|
|
11
|
+
* so Task Scheduler can invoke node with the palmier script + subcommand.
|
|
14
12
|
*/
|
|
15
|
-
function
|
|
16
|
-
// process.argv[1] is the script path; wrap with node so it works as
|
|
17
|
-
// a Task Scheduler command without relying on file associations.
|
|
13
|
+
function schtasksTr(...subcommand) {
|
|
18
14
|
const script = process.argv[1] || "palmier";
|
|
19
|
-
return `"${process.execPath}" "${script}"`;
|
|
15
|
+
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
20
16
|
}
|
|
21
17
|
/**
|
|
22
18
|
* Convert one of the 4 supported cron patterns to schtasks flags.
|
|
@@ -65,56 +61,81 @@ function schtasksTaskName(taskId) {
|
|
|
65
61
|
}
|
|
66
62
|
export class WindowsPlatform {
|
|
67
63
|
installDaemon(config) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const baseArgs = [
|
|
71
|
-
"/create", "/tn", DAEMON_TASK_NAME,
|
|
72
|
-
"/tr", `${cmd} serve`,
|
|
73
|
-
"/rl", "HIGHEST",
|
|
74
|
-
"/f", // force overwrite if exists
|
|
75
|
-
];
|
|
64
|
+
const script = process.argv[1] || "palmier";
|
|
65
|
+
const regValue = `"${process.execPath}" "${script}" serve`;
|
|
76
66
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
execFileSync("reg", [
|
|
68
|
+
"add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
|
|
69
|
+
"/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
|
|
70
|
+
], { encoding: "utf-8", stdio: "pipe" });
|
|
71
|
+
console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
79
72
|
}
|
|
80
|
-
catch {
|
|
81
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(`Warning: failed to install registry run entry: ${err}`);
|
|
75
|
+
console.error("You may need to start palmier serve manually.");
|
|
76
|
+
}
|
|
77
|
+
// Start the daemon now
|
|
78
|
+
this.spawnDaemon(script);
|
|
79
|
+
console.log("\nHost initialization complete!");
|
|
80
|
+
}
|
|
81
|
+
async restartDaemon() {
|
|
82
|
+
// Kill the old daemon if we have its PID
|
|
83
|
+
if (fs.existsSync(DAEMON_PID_FILE)) {
|
|
84
|
+
const oldPid = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
|
|
82
85
|
try {
|
|
83
|
-
|
|
84
|
-
console.log(`Task Scheduler: "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
85
|
-
console.log(" Tip: run as Administrator to use ONSTART instead.");
|
|
86
|
+
execFileSync("taskkill", ["/pid", oldPid, "/t", "/f"], { windowsHide: true });
|
|
86
87
|
}
|
|
87
|
-
catch
|
|
88
|
-
|
|
89
|
-
console.error("You may need to start palmier serve manually.");
|
|
88
|
+
catch {
|
|
89
|
+
// Process may have already exited
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const script = process.argv[1] || "palmier";
|
|
93
|
+
this.spawnDaemon(script);
|
|
94
|
+
}
|
|
95
|
+
spawnDaemon(script) {
|
|
96
|
+
const child = nodeSpawn(process.execPath, [script, "serve"], {
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: "ignore",
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
if (child.pid) {
|
|
102
|
+
fs.writeFileSync(DAEMON_PID_FILE, String(child.pid), "utf-8");
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
child.unref();
|
|
105
|
+
console.log("Palmier daemon started.");
|
|
101
106
|
}
|
|
102
107
|
installTaskTimer(config, task) {
|
|
103
108
|
const taskId = task.frontmatter.id;
|
|
104
109
|
const tn = schtasksTaskName(taskId);
|
|
105
|
-
const
|
|
110
|
+
const tr = schtasksTr("run", taskId);
|
|
111
|
+
// Always create the scheduled task with a dummy trigger first.
|
|
112
|
+
// This ensures startTask (/run) works even when no triggers are configured.
|
|
113
|
+
try {
|
|
114
|
+
execFileSync("schtasks", [
|
|
115
|
+
"/create", "/tn", tn,
|
|
116
|
+
"/tr", tr,
|
|
117
|
+
"/sc", "ONCE", "/sd", "01/01/2000", "/st", "00:00",
|
|
118
|
+
"/f",
|
|
119
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const e = err;
|
|
123
|
+
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
124
|
+
}
|
|
125
|
+
// Overlay with real schedule triggers if enabled
|
|
126
|
+
if (!task.frontmatter.triggers_enabled)
|
|
127
|
+
return;
|
|
106
128
|
const triggers = task.frontmatter.triggers || [];
|
|
107
129
|
for (const trigger of triggers) {
|
|
108
130
|
if (trigger.type === "cron") {
|
|
109
131
|
const schedArgs = cronToSchtasksArgs(trigger.value);
|
|
110
|
-
const args = [
|
|
111
|
-
"/create", "/tn", tn,
|
|
112
|
-
"/tr", `${cmd} run ${taskId}`,
|
|
113
|
-
...schedArgs,
|
|
114
|
-
"/f",
|
|
115
|
-
];
|
|
116
132
|
try {
|
|
117
|
-
|
|
133
|
+
execFileSync("schtasks", [
|
|
134
|
+
"/create", "/tn", tn,
|
|
135
|
+
"/tr", tr,
|
|
136
|
+
...schedArgs,
|
|
137
|
+
"/f",
|
|
138
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
118
139
|
}
|
|
119
140
|
catch (err) {
|
|
120
141
|
const e = err;
|
|
@@ -132,14 +153,13 @@ export class WindowsPlatform {
|
|
|
132
153
|
const [year, month, day] = datePart.split("-");
|
|
133
154
|
const sd = `${month}/${day}/${year}`;
|
|
134
155
|
const st = timePart.slice(0, 5);
|
|
135
|
-
const args = [
|
|
136
|
-
"/create", "/tn", tn,
|
|
137
|
-
"/tr", `${cmd} run ${taskId}`,
|
|
138
|
-
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
139
|
-
"/f",
|
|
140
|
-
];
|
|
141
156
|
try {
|
|
142
|
-
|
|
157
|
+
execFileSync("schtasks", [
|
|
158
|
+
"/create", "/tn", tn,
|
|
159
|
+
"/tr", tr,
|
|
160
|
+
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
161
|
+
"/f",
|
|
162
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
143
163
|
}
|
|
144
164
|
catch (err) {
|
|
145
165
|
const e = err;
|
|
@@ -151,46 +171,43 @@ export class WindowsPlatform {
|
|
|
151
171
|
removeTaskTimer(taskId) {
|
|
152
172
|
const tn = schtasksTaskName(taskId);
|
|
153
173
|
try {
|
|
154
|
-
|
|
174
|
+
execFileSync("schtasks", ["/delete", "/tn", tn, "/f"], { encoding: "utf-8", windowsHide: true });
|
|
155
175
|
}
|
|
156
176
|
catch {
|
|
157
177
|
// Task might not exist — that's fine
|
|
158
178
|
}
|
|
159
179
|
}
|
|
160
180
|
async startTask(taskId) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
cwd: config.projectRoot,
|
|
169
|
-
});
|
|
170
|
-
if (child.pid) {
|
|
171
|
-
fs.mkdirSync(taskDir, { recursive: true });
|
|
172
|
-
fs.writeFileSync(path.join(taskDir, "pid"), String(child.pid), "utf-8");
|
|
181
|
+
const tn = schtasksTaskName(taskId);
|
|
182
|
+
try {
|
|
183
|
+
execFileSync("schtasks", ["/run", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const e = err;
|
|
187
|
+
throw new Error(`Failed to start task via schtasks: ${e.stderr || e.message}`);
|
|
173
188
|
}
|
|
174
|
-
child.unref();
|
|
175
189
|
}
|
|
176
190
|
async stopTask(taskId) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!fs.existsSync(pidPath)) {
|
|
181
|
-
throw new Error(`No PID file found for task ${taskId}`);
|
|
191
|
+
const tn = schtasksTaskName(taskId);
|
|
192
|
+
try {
|
|
193
|
+
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
182
194
|
}
|
|
183
|
-
|
|
195
|
+
catch (err) {
|
|
196
|
+
const e = err;
|
|
197
|
+
throw new Error(`Failed to stop task via schtasks: ${e.stderr || e.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
isTaskRunning(taskId) {
|
|
201
|
+
const tn = schtasksTaskName(taskId);
|
|
184
202
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
203
|
+
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
204
|
+
encoding: "utf-8",
|
|
205
|
+
windowsHide: true,
|
|
206
|
+
});
|
|
207
|
+
return out.includes('"Running"');
|
|
187
208
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
fs.unlinkSync(pidPath);
|
|
192
|
-
}
|
|
193
|
-
catch { /* ignore */ }
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
194
211
|
}
|
|
195
212
|
}
|
|
196
213
|
getGuiEnv() {
|
package/dist/rpc-handler.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type NatsConnection } from "nats";
|
|
1
2
|
import type { HostConfig, RpcMessage } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
4
5
|
*/
|
|
5
|
-
export declare function createRpcHandler(config: HostConfig): (request: RpcMessage) => Promise<unknown>;
|
|
6
|
+
export declare function createRpcHandler(config: HostConfig, nc?: NatsConnection): (request: RpcMessage) => Promise<unknown>;
|
|
6
7
|
//# sourceMappingURL=rpc-handler.d.ts.map
|
package/dist/rpc-handler.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import * as os from "os";
|
|
3
2
|
import * as fs from "fs";
|
|
4
3
|
import * as path from "path";
|
|
5
4
|
import { fileURLToPath } from "url";
|
|
@@ -9,19 +8,9 @@ import { getPlatform } from "./platform/index.js";
|
|
|
9
8
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
9
|
import { getAgent } from "./agents/agent.js";
|
|
11
10
|
import { validateSession } from "./session-store.js";
|
|
11
|
+
import { publishHostEvent } from "./events.js";
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
14
|
-
function detectLanIp() {
|
|
15
|
-
const interfaces = os.networkInterfaces();
|
|
16
|
-
for (const name of Object.keys(interfaces)) {
|
|
17
|
-
for (const iface of interfaces[name] ?? []) {
|
|
18
|
-
if (iface.family === "IPv4" && !iface.internal) {
|
|
19
|
-
return iface.address;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return "127.0.0.1";
|
|
24
|
-
}
|
|
25
14
|
/**
|
|
26
15
|
* Parse RESULT frontmatter into a metadata object.
|
|
27
16
|
*/
|
|
@@ -71,11 +60,12 @@ function parseResultFrontmatter(raw) {
|
|
|
71
60
|
async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
72
61
|
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
73
62
|
const planAgent = getAgent(agentName);
|
|
74
|
-
const { command, args } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
63
|
+
const { command, args, stdin } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
75
64
|
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
76
65
|
const { output } = await spawnCommand(command, args, {
|
|
77
66
|
cwd: projectRoot,
|
|
78
67
|
timeout: 120_000,
|
|
68
|
+
stdin,
|
|
79
69
|
});
|
|
80
70
|
let name = "";
|
|
81
71
|
const trimmed = output.trim();
|
|
@@ -96,7 +86,7 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
|
96
86
|
/**
|
|
97
87
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
98
88
|
*/
|
|
99
|
-
export function createRpcHandler(config) {
|
|
89
|
+
export function createRpcHandler(config, nc) {
|
|
100
90
|
function flattenTask(task) {
|
|
101
91
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
102
92
|
return {
|
|
@@ -121,10 +111,10 @@ export function createRpcHandler(config) {
|
|
|
121
111
|
}
|
|
122
112
|
case "task.create": {
|
|
123
113
|
const params = request.params;
|
|
124
|
-
//
|
|
114
|
+
// Only generate a plan for longer prompts that benefit from it
|
|
125
115
|
let name = "";
|
|
126
116
|
let body = "";
|
|
127
|
-
if (params.user_prompt.length
|
|
117
|
+
if (params.user_prompt.length <= 50) {
|
|
128
118
|
name = params.user_prompt;
|
|
129
119
|
}
|
|
130
120
|
else {
|
|
@@ -149,15 +139,13 @@ export function createRpcHandler(config) {
|
|
|
149
139
|
triggers: params.triggers ?? [],
|
|
150
140
|
triggers_enabled: params.triggers_enabled ?? true,
|
|
151
141
|
requires_confirmation: params.requires_confirmation ?? true,
|
|
142
|
+
...(params.command ? { command: params.command } : {}),
|
|
152
143
|
},
|
|
153
144
|
body,
|
|
154
145
|
};
|
|
155
146
|
writeTaskFile(taskDir, task);
|
|
156
147
|
appendTaskList(config.projectRoot, id);
|
|
157
|
-
|
|
158
|
-
if (task.frontmatter.triggers_enabled) {
|
|
159
|
-
platform.installTaskTimer(config, task);
|
|
160
|
-
}
|
|
148
|
+
getPlatform().installTaskTimer(config, task);
|
|
161
149
|
return flattenTask(task);
|
|
162
150
|
}
|
|
163
151
|
case "task.update": {
|
|
@@ -179,31 +167,34 @@ export function createRpcHandler(config) {
|
|
|
179
167
|
existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
180
168
|
if (params.requires_confirmation !== undefined)
|
|
181
169
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
existing.frontmatter.name = existing.frontmatter.user_prompt;
|
|
186
|
-
existing.body = "";
|
|
170
|
+
if (params.command !== undefined) {
|
|
171
|
+
if (params.command) {
|
|
172
|
+
existing.frontmatter.command = params.command;
|
|
187
173
|
}
|
|
188
174
|
else {
|
|
189
|
-
|
|
190
|
-
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
191
|
-
existing.frontmatter.name = plan.name;
|
|
192
|
-
existing.body = plan.body;
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
const error = err;
|
|
196
|
-
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
197
|
-
}
|
|
175
|
+
delete existing.frontmatter.command;
|
|
198
176
|
}
|
|
199
177
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (existing.frontmatter.triggers_enabled) {
|
|
205
|
-
platform.installTaskTimer(config, existing);
|
|
178
|
+
// Regenerate plan if needed (only for longer prompts)
|
|
179
|
+
if (existing.frontmatter.user_prompt.length <= 50) {
|
|
180
|
+
existing.frontmatter.name = existing.frontmatter.user_prompt;
|
|
181
|
+
existing.body = "";
|
|
206
182
|
}
|
|
183
|
+
else if (needsRegeneration) {
|
|
184
|
+
try {
|
|
185
|
+
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
186
|
+
existing.frontmatter.name = plan.name;
|
|
187
|
+
existing.body = plan.body;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const error = err;
|
|
191
|
+
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
writeTaskFile(taskDir, existing);
|
|
195
|
+
// Update timers — installTaskTimer overwrites in-place (schtasks /f,
|
|
196
|
+
// systemd unit rewrite) without killing a running task process.
|
|
197
|
+
getPlatform().installTaskTimer(config, existing);
|
|
207
198
|
return flattenTask(existing);
|
|
208
199
|
}
|
|
209
200
|
case "task.delete": {
|
|
@@ -226,15 +217,25 @@ export function createRpcHandler(config) {
|
|
|
226
217
|
}
|
|
227
218
|
case "task.abort": {
|
|
228
219
|
const params = request.params;
|
|
220
|
+
// Write abort status BEFORE killing so the dying process's signal
|
|
221
|
+
// handler can detect this was RPC-initiated and skip publishing.
|
|
222
|
+
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
223
|
+
writeTaskStatus(abortTaskDir, {
|
|
224
|
+
running_state: "aborted",
|
|
225
|
+
time_stamp: Date.now(),
|
|
226
|
+
});
|
|
229
227
|
try {
|
|
230
228
|
await getPlatform().stopTask(params.id);
|
|
231
|
-
return { ok: true, task_id: params.id };
|
|
232
229
|
}
|
|
233
230
|
catch (err) {
|
|
234
231
|
const e = err;
|
|
235
232
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
236
233
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
237
234
|
}
|
|
235
|
+
// Notify connected clients (NATS + HTTP SSE if LAN server is running)
|
|
236
|
+
const abortPayload = { event_type: "running-state", running_state: "aborted" };
|
|
237
|
+
await publishHostEvent(nc, config.hostId, params.id, abortPayload);
|
|
238
|
+
return { ok: true, task_id: params.id };
|
|
238
239
|
}
|
|
239
240
|
case "task.status": {
|
|
240
241
|
const params = request.params;
|
|
@@ -291,7 +292,7 @@ export function createRpcHandler(config) {
|
|
|
291
292
|
const params = request.params;
|
|
292
293
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
293
294
|
const currentStatus = readTaskStatus(taskDir);
|
|
294
|
-
if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length) {
|
|
295
|
+
if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length && !currentStatus?.pending_input?.length) {
|
|
295
296
|
return { ok: false, error: "not pending" };
|
|
296
297
|
}
|
|
297
298
|
writeTaskStatus(taskDir, { ...currentStatus, user_input: params.value });
|
|
@@ -331,16 +332,6 @@ export function createRpcHandler(config) {
|
|
|
331
332
|
}
|
|
332
333
|
return { ok: true, task_id: params.task_id, result_file: params.result_file };
|
|
333
334
|
}
|
|
334
|
-
case "host.directInfo": {
|
|
335
|
-
if (config.mode === "lan" || config.mode === "auto") {
|
|
336
|
-
const ip = detectLanIp();
|
|
337
|
-
return {
|
|
338
|
-
directUrl: `http://${ip}:${config.directPort ?? 7400}`,
|
|
339
|
-
directToken: config.directToken,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
return { directUrl: null, directToken: null };
|
|
343
|
-
}
|
|
344
335
|
default:
|
|
345
336
|
return { error: `Unknown method: ${request.method}` };
|
|
346
337
|
}
|
package/dist/spawn-command.d.ts
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
+
import type { ChildProcess } from "child_process";
|
|
2
|
+
export interface SpawnStreamingOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Spawn a command with shell interpretation, returning the ChildProcess
|
|
8
|
+
* with stdout piped for line-by-line reading.
|
|
9
|
+
*
|
|
10
|
+
* Unlike spawnCommand(), this does NOT collect output into a buffer —
|
|
11
|
+
* the caller reads from child.stdout directly (e.g. via readline).
|
|
12
|
+
*
|
|
13
|
+
* shell: true is required so users can write piped commands like
|
|
14
|
+
* "tail -f log | grep ERROR".
|
|
15
|
+
*
|
|
16
|
+
* stdin is "pipe" (kept open, never written to) rather than "ignore"
|
|
17
|
+
* (/dev/null). Some long-running commands exit when stdin is closed/EOF.
|
|
18
|
+
* This differs from spawnCommand() which uses "ignore" because agent
|
|
19
|
+
* CLIs like `claude -p` hang on an open stdin pipe.
|
|
20
|
+
*/
|
|
21
|
+
export declare function spawnStreamingCommand(command: string, opts: SpawnStreamingOptions): ChildProcess;
|
|
1
22
|
export interface SpawnCommandOptions {
|
|
2
23
|
cwd: string;
|
|
3
24
|
env?: Record<string, string>;
|
|
4
25
|
timeout?: number;
|
|
5
26
|
/** Echo stdout to process.stdout (useful for journald logging). */
|
|
6
27
|
echoStdout?: boolean;
|
|
7
|
-
/** Forward SIGINT/SIGTERM to the child and resolve on stop. */
|
|
8
|
-
forwardSignals?: boolean;
|
|
9
28
|
/** Resolve with output even on non-zero exit (instead of rejecting). */
|
|
10
29
|
resolveOnFailure?: boolean;
|
|
30
|
+
/** If provided, write this string to the process's stdin and then close the pipe. */
|
|
31
|
+
stdin?: string;
|
|
11
32
|
}
|
|
12
33
|
/**
|
|
13
34
|
* Spawn a command with additional arguments.
|
|
14
35
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
36
|
+
* Uses cross-spawn to correctly resolve .cmd shims and escape arguments
|
|
37
|
+
* on Windows without shell: true (which mishandles special characters).
|
|
17
38
|
*
|
|
18
39
|
* On other platforms the command is executed directly (no shell), so no
|
|
19
40
|
* escaping is needed.
|
|
20
41
|
*
|
|
21
|
-
* stdin is set to "ignore" (equivalent to < /dev/null) because
|
|
22
|
-
* `claude -p` hang indefinitely on an open stdin pipe.
|
|
42
|
+
* stdin is set to "ignore" by default (equivalent to < /dev/null) because
|
|
43
|
+
* tools like `claude -p` hang indefinitely on an open stdin pipe.
|
|
44
|
+
* When opts.stdin is provided, stdin is set to "pipe" and the string is
|
|
45
|
+
* written to the process before closing the pipe.
|
|
23
46
|
*/
|
|
24
47
|
export interface SpawnCommandResult {
|
|
25
48
|
output: string;
|
package/dist/spawn-command.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crossSpawn from "cross-spawn";
|
|
2
|
+
/**
|
|
3
|
+
* Spawn a command with shell interpretation, returning the ChildProcess
|
|
4
|
+
* with stdout piped for line-by-line reading.
|
|
5
|
+
*
|
|
6
|
+
* Unlike spawnCommand(), this does NOT collect output into a buffer —
|
|
7
|
+
* the caller reads from child.stdout directly (e.g. via readline).
|
|
8
|
+
*
|
|
9
|
+
* shell: true is required so users can write piped commands like
|
|
10
|
+
* "tail -f log | grep ERROR".
|
|
11
|
+
*
|
|
12
|
+
* stdin is "pipe" (kept open, never written to) rather than "ignore"
|
|
13
|
+
* (/dev/null). Some long-running commands exit when stdin is closed/EOF.
|
|
14
|
+
* This differs from spawnCommand() which uses "ignore" because agent
|
|
15
|
+
* CLIs like `claude -p` hang on an open stdin pipe.
|
|
16
|
+
*/
|
|
17
|
+
export function spawnStreamingCommand(command, opts) {
|
|
18
|
+
return crossSpawn(command, [], {
|
|
19
|
+
cwd: opts.cwd,
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
shell: true,
|
|
22
|
+
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
2
26
|
export function spawnCommand(command, args, opts) {
|
|
3
27
|
return new Promise((resolve, reject) => {
|
|
4
|
-
|
|
28
|
+
// Collapse newlines to spaces — cmd.exe can't handle literal newlines
|
|
29
|
+
// in arguments, and CLI prompts don't need them.
|
|
30
|
+
const finalArgs = process.platform === "win32"
|
|
31
|
+
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
32
|
+
: args;
|
|
33
|
+
// console.log(`[spawn] ${command} ${finalArgs.join(" ")}`);
|
|
34
|
+
const child = crossSpawn(command, finalArgs, {
|
|
5
35
|
cwd: opts.cwd,
|
|
6
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
7
37
|
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
8
|
-
|
|
9
|
-
shell: process.platform === "win32",
|
|
38
|
+
windowsHide: true,
|
|
10
39
|
});
|
|
40
|
+
if (opts.stdin != null) {
|
|
41
|
+
child.stdin.end(opts.stdin);
|
|
42
|
+
}
|
|
11
43
|
const chunks = [];
|
|
12
44
|
child.stdout.on("data", (d) => {
|
|
13
45
|
chunks.push(d);
|
|
@@ -15,15 +47,6 @@ export function spawnCommand(command, args, opts) {
|
|
|
15
47
|
process.stdout.write(d);
|
|
16
48
|
});
|
|
17
49
|
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
18
|
-
let stopping = false;
|
|
19
|
-
if (opts.forwardSignals) {
|
|
20
|
-
const killChild = () => {
|
|
21
|
-
stopping = true;
|
|
22
|
-
child.kill("SIGTERM");
|
|
23
|
-
};
|
|
24
|
-
process.on("SIGINT", killChild);
|
|
25
|
-
process.on("SIGTERM", killChild);
|
|
26
|
-
}
|
|
27
50
|
let timer;
|
|
28
51
|
if (opts.timeout) {
|
|
29
52
|
timer = setTimeout(() => {
|
|
@@ -35,7 +58,7 @@ export function spawnCommand(command, args, opts) {
|
|
|
35
58
|
if (timer)
|
|
36
59
|
clearTimeout(timer);
|
|
37
60
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
38
|
-
if (code === 0 ||
|
|
61
|
+
if (code === 0 || opts.resolveOnFailure)
|
|
39
62
|
resolve({ output, exitCode: code });
|
|
40
63
|
else
|
|
41
64
|
reject(new Error(`process exited with code ${code}`));
|
|
@@ -3,5 +3,5 @@ export declare function detectLanIp(): string;
|
|
|
3
3
|
/**
|
|
4
4
|
* Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
|
|
5
5
|
*/
|
|
6
|
-
export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown
|
|
6
|
+
export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, port: number, pairingCode?: string, onReady?: () => void): Promise<void>;
|
|
7
7
|
//# sourceMappingURL=http-transport.d.ts.map
|