palmier 0.8.1 → 0.8.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.
- package/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +12 -16
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/{index-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +18 -47
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +165 -20
- package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +20 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +38 -7
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +3 -2
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +12 -18
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +19 -47
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-DQfOEB03.js +0 -120
package/src/agents/cursor.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class Cursor implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--force");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("--continue");}
|
|
22
|
+
if (followupPrompt) {args.push("--continue");}
|
|
23
23
|
args.push("-p", prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "cursor", args};
|
package/src/agents/deepagents.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class DeepAgents implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--auto-approve");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("--resume");}
|
|
22
|
+
if (followupPrompt) {args.push("--resume");}
|
|
23
23
|
args.push("--non-interactive", prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "deepagents", args};
|
package/src/agents/gemini.ts
CHANGED
|
@@ -25,8 +25,9 @@ export class GeminiAgent implements AgentTool {
|
|
|
25
25
|
args.push("--allowed-tools", tools.join(","));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (followupPrompt) {args.push("--resume");}
|
|
29
|
-
|
|
28
|
+
if (followupPrompt) {args.push("--resume");}
|
|
29
|
+
// Read prompt from stdin to avoid command-line length limits.
|
|
30
|
+
args.push("--prompt", "-");
|
|
30
31
|
|
|
31
32
|
return { command: "gemini", args, stdin: prompt };
|
|
32
33
|
}
|
package/src/agents/goose.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class GooseAgent implements AgentTool {
|
|
|
16
16
|
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
17
17
|
const args = ["run"];
|
|
18
18
|
|
|
19
|
-
if (followupPrompt) {args.push("--resume");}
|
|
19
|
+
if (followupPrompt) {args.push("--resume");}
|
|
20
20
|
args.push("--text", prompt);
|
|
21
21
|
|
|
22
22
|
return { command: "goose", args, ...(yolo ? { env: { GOOSE_MODE: "auto" } } : {}) };
|
package/src/agents/hermes.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class Hermes implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--trust-all-tools");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("--continue");}
|
|
22
|
+
if (followupPrompt) {args.push("--continue");}
|
|
23
23
|
args.push("-q", prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "hermes", args};
|
package/src/agents/kiro.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class Kiro implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--trust-all-tools");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("--resume");}
|
|
22
|
+
if (followupPrompt) {args.push("--resume");}
|
|
23
23
|
args.push("--no-interactive", prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "kiro-cli", args};
|
package/src/agents/opencode.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class OpenCodeAgent implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--dangerously-skip-permissions");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("--continue");}
|
|
22
|
+
if (followupPrompt) {args.push("--continue");}
|
|
23
23
|
args.push(prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "opencode", args};
|
package/src/agents/qoder.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class Qoder implements AgentTool {
|
|
|
19
19
|
if (yolo) {
|
|
20
20
|
args.push("--yolo");
|
|
21
21
|
}
|
|
22
|
-
if (followupPrompt) {args.push("-c");}
|
|
22
|
+
if (followupPrompt) {args.push("-c");}
|
|
23
23
|
args.push("-p", prompt);
|
|
24
24
|
|
|
25
25
|
return { command: "qodercli", args};
|
|
@@ -12,9 +12,6 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
12
12
|
"utf-8",
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Build the full agent prompt: instructions + endpoint docs + task description.
|
|
17
|
-
*/
|
|
18
15
|
export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
|
|
19
16
|
const port = loadConfig().httpPort ?? 7256;
|
|
20
17
|
const taskDescription = task.frontmatter.user_prompt;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
|
|
5
|
+
const REGISTRY_FILE = path.join(CONFIG_DIR, "app-registry.json");
|
|
6
|
+
|
|
7
|
+
/** Persistent cache of packageName → appName pairs seen via device notifications. */
|
|
8
|
+
export interface AppInfo {
|
|
9
|
+
packageName: string;
|
|
10
|
+
appName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let cache: Record<string, string> | null = null;
|
|
14
|
+
|
|
15
|
+
function load(): Record<string, string> {
|
|
16
|
+
if (cache) return cache;
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
19
|
+
cache = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf-8")) as Record<string, string>;
|
|
20
|
+
return cache;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Corrupt file — start fresh rather than fail notifications.
|
|
24
|
+
}
|
|
25
|
+
cache = {};
|
|
26
|
+
return cache;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function persist(map: Record<string, string>): void {
|
|
30
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(map, null, 2), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Writes only on change so we track the latest label if an app renames itself. */
|
|
35
|
+
export function recordApp(packageName: string, appName: string): void {
|
|
36
|
+
if (!packageName || !appName) return;
|
|
37
|
+
const map = load();
|
|
38
|
+
if (map[packageName] === appName) return;
|
|
39
|
+
map[packageName] = appName;
|
|
40
|
+
persist(map);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function listApps(): AppInfo[] {
|
|
44
|
+
const map = load();
|
|
45
|
+
return Object.entries(map)
|
|
46
|
+
.map(([packageName, appName]) => ({ packageName, appName }))
|
|
47
|
+
.sort((a, b) => a.appName.localeCompare(b.appName));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getAppName(packageName: string): string | undefined {
|
|
51
|
+
return load()[packageName];
|
|
52
|
+
}
|
package/src/commands/info.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { loadClients } from "../client-store.js";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Print host connection info for setting up clients.
|
|
6
|
-
*/
|
|
7
4
|
export async function infoCommand(): Promise<void> {
|
|
8
5
|
const config = loadConfig();
|
|
9
6
|
const clients = loadClients();
|
|
@@ -11,14 +8,12 @@ export async function infoCommand(): Promise<void> {
|
|
|
11
8
|
console.log(`Host ID: ${config.hostId}`);
|
|
12
9
|
console.log(`Project root: ${config.projectRoot}`);
|
|
13
10
|
|
|
14
|
-
// Detected agents
|
|
15
11
|
if (config.agents && config.agents.length > 0) {
|
|
16
12
|
console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
|
|
17
13
|
} else {
|
|
18
14
|
console.log(`Agents: (none detected — run \`palmier agents\`)`);
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
// Clients
|
|
22
17
|
console.log(`Clients: ${clients.length} active`);
|
|
23
18
|
|
|
24
19
|
if (clients.length === 0) {
|
package/src/commands/init.ts
CHANGED
|
@@ -15,9 +15,6 @@ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
|
15
15
|
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
16
16
|
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* Interactive wizard to provision this host.
|
|
20
|
-
*/
|
|
21
18
|
export async function initCommand(): Promise<void> {
|
|
22
19
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
23
20
|
const ask: AskFn = (q) => new Promise<string>((resolve) => rl.question(q, resolve));
|
|
@@ -27,7 +24,6 @@ export async function initCommand(): Promise<void> {
|
|
|
27
24
|
console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
|
|
28
25
|
console.log(`and ${cyan("Privacy Policy")} (https://www.palmier.me/privacy).\n`);
|
|
29
26
|
|
|
30
|
-
// Detect agents first — abort if none found
|
|
31
27
|
console.log("Detecting installed agents...");
|
|
32
28
|
const agents = await detectAgents();
|
|
33
29
|
|
|
@@ -41,7 +37,6 @@ export async function initCommand(): Promise<void> {
|
|
|
41
37
|
|
|
42
38
|
console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
|
|
43
39
|
|
|
44
|
-
// LAN mode
|
|
45
40
|
const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
|
|
46
41
|
const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
|
|
47
42
|
|
|
@@ -51,7 +46,6 @@ export async function initCommand(): Promise<void> {
|
|
|
51
46
|
const parsed = parseInt(portAnswer.trim(), 10);
|
|
52
47
|
if (parsed > 0 && parsed < 65536) httpPort = parsed;
|
|
53
48
|
|
|
54
|
-
// Display summary and ask for confirmation before making any changes
|
|
55
49
|
console.log(`\n${bold("Setup summary:")}\n`);
|
|
56
50
|
console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
|
|
57
51
|
console.log(` All tasks and execution data will be stored here.\n`);
|
|
@@ -64,7 +58,6 @@ export async function initCommand(): Promise<void> {
|
|
|
64
58
|
}
|
|
65
59
|
console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
|
|
66
60
|
|
|
67
|
-
// Check for existing tasks to recover
|
|
68
61
|
const existingTasks = listTasks(process.cwd());
|
|
69
62
|
if (existingTasks.length > 0) {
|
|
70
63
|
console.log(` ${dim("Recover tasks:")} ${existingTasks.length} existing task(s) found:`);
|
|
@@ -81,7 +74,6 @@ export async function initCommand(): Promise<void> {
|
|
|
81
74
|
return;
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
// Register with server
|
|
85
77
|
let existingHostId: string | undefined;
|
|
86
78
|
try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
|
|
87
79
|
|
|
@@ -105,7 +97,6 @@ export async function initCommand(): Promise<void> {
|
|
|
105
97
|
}
|
|
106
98
|
}
|
|
107
99
|
|
|
108
|
-
// Build and save config
|
|
109
100
|
const config: HostConfig = {
|
|
110
101
|
hostId: registerResponse.hostId,
|
|
111
102
|
projectRoot: process.cwd(),
|
|
@@ -123,8 +114,8 @@ export async function initCommand(): Promise<void> {
|
|
|
123
114
|
|
|
124
115
|
getPlatform().installDaemon(config);
|
|
125
116
|
|
|
126
|
-
// Task recovery
|
|
127
|
-
//
|
|
117
|
+
// Task recovery runs in the daemon (palmier serve) because that process
|
|
118
|
+
// is elevated and can create S4U scheduled tasks.
|
|
128
119
|
|
|
129
120
|
console.log("\nStarting pairing...");
|
|
130
121
|
rl.close();
|
package/src/commands/pair.ts
CHANGED
|
@@ -24,9 +24,6 @@ function buildPairResponse(config: HostConfig, label?: string) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* POST to the running serve daemon and long-poll until paired or expired.
|
|
29
|
-
*/
|
|
30
27
|
function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
31
28
|
const body = JSON.stringify({ code, expiryMs: PAIRING_EXPIRY_MS });
|
|
32
29
|
|
|
@@ -60,10 +57,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
60
57
|
});
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
/**
|
|
64
|
-
* Generate a pairing code and wait for a PWA client to pair.
|
|
65
|
-
* Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
|
|
66
|
-
*/
|
|
60
|
+
/** Listens on NATS (server mode) and HTTP (via serve daemon) in parallel. */
|
|
67
61
|
export async function pairCommand(): Promise<void> {
|
|
68
62
|
const config = loadConfig();
|
|
69
63
|
const code = generatePairingCode();
|
|
@@ -78,7 +72,6 @@ export async function pairCommand(): Promise<void> {
|
|
|
78
72
|
|
|
79
73
|
const cleanups: Array<() => void | Promise<void>> = [];
|
|
80
74
|
|
|
81
|
-
// Display pairing info
|
|
82
75
|
console.log("");
|
|
83
76
|
console.log("Enter this code in your Palmier app:");
|
|
84
77
|
console.log("");
|
|
@@ -86,7 +79,6 @@ export async function pairCommand(): Promise<void> {
|
|
|
86
79
|
console.log("");
|
|
87
80
|
console.log("Code expires in 1 minute.");
|
|
88
81
|
|
|
89
|
-
// NATS pairing (server mode)
|
|
90
82
|
const nc = await connectNats(config);
|
|
91
83
|
const sc = StringCodec();
|
|
92
84
|
const subject = `pair.${code}`;
|
|
@@ -116,13 +108,11 @@ export async function pairCommand(): Promise<void> {
|
|
|
116
108
|
}
|
|
117
109
|
})();
|
|
118
110
|
|
|
119
|
-
// HTTP pairing — register with serve daemon's /pair-register endpoint
|
|
120
111
|
(async () => {
|
|
121
112
|
const result = await httpPairRegister(httpPort, code);
|
|
122
113
|
if (result) onPaired();
|
|
123
114
|
})();
|
|
124
115
|
|
|
125
|
-
// Wait for pairing or timeout
|
|
126
116
|
const start = Date.now();
|
|
127
117
|
await new Promise<void>((resolve) => {
|
|
128
118
|
const interval = setInterval(() => {
|
|
@@ -133,7 +123,6 @@ export async function pairCommand(): Promise<void> {
|
|
|
133
123
|
}, 500);
|
|
134
124
|
});
|
|
135
125
|
|
|
136
|
-
// Cleanup
|
|
137
126
|
for (const cleanup of cleanups) {
|
|
138
127
|
await cleanup();
|
|
139
128
|
}
|
package/src/commands/restart.ts
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -13,10 +13,6 @@ import { publishHostEvent } from "../events.js";
|
|
|
13
13
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
14
14
|
import type { NatsConnection } from "nats";
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Shared context for agent invocation retry loops.
|
|
18
|
-
* Passed around to avoid threading many individual parameters.
|
|
19
|
-
*/
|
|
20
16
|
interface InvocationContext {
|
|
21
17
|
agent: AgentTool;
|
|
22
18
|
task: ParsedTask;
|
|
@@ -35,11 +31,9 @@ interface InvocationResult {
|
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
/**
|
|
38
|
-
* Invoke the agent CLI
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
42
|
-
* (for command-triggered mode this is the per-line augmented task).
|
|
34
|
+
* Invoke the agent CLI in a continuation loop to handle permission requests.
|
|
35
|
+
* `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
|
|
36
|
+
* command-triggered mode this is the per-line augmented task).
|
|
43
37
|
*/
|
|
44
38
|
async function invokeAgentWithRetries(
|
|
45
39
|
ctx: InvocationContext,
|
|
@@ -47,7 +41,6 @@ async function invokeAgentWithRetries(
|
|
|
47
41
|
): Promise<InvocationResult> {
|
|
48
42
|
// eslint-disable-next-line no-constant-condition
|
|
49
43
|
while (true) {
|
|
50
|
-
// Stream agent output to TASKRUN.md in real-time, throttled to 500ms
|
|
51
44
|
const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
|
|
52
45
|
let lineBuf = "";
|
|
53
46
|
let notifyPending = false;
|
|
@@ -89,12 +82,10 @@ async function invokeAgentWithRetries(
|
|
|
89
82
|
const reportFiles = parseReportFiles(result.output);
|
|
90
83
|
const requiredPermissions = parsePermissions(result.output);
|
|
91
84
|
|
|
92
|
-
// Flush remaining buffered content
|
|
93
85
|
if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
|
|
94
86
|
writer.write(lineBuf);
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
// Include permission requests in the assistant message
|
|
98
89
|
if (requiredPermissions.length > 0) {
|
|
99
90
|
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
100
91
|
writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
@@ -112,7 +103,6 @@ async function invokeAgentWithRetries(
|
|
|
112
103
|
});
|
|
113
104
|
}
|
|
114
105
|
|
|
115
|
-
// Permission handling — agent requested permissions
|
|
116
106
|
if (requiredPermissions.length > 0) {
|
|
117
107
|
const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
118
108
|
|
|
@@ -146,27 +136,20 @@ async function invokeAgentWithRetries(
|
|
|
146
136
|
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
//
|
|
139
|
+
// Retry with the new permissions if the agent failed.
|
|
150
140
|
if (outcome === "failed") {
|
|
151
141
|
continue;
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
144
|
|
|
155
|
-
// Normal completion (success or terminal failure)
|
|
156
145
|
return { outcome };
|
|
157
146
|
}
|
|
158
147
|
}
|
|
159
148
|
|
|
160
|
-
/**
|
|
161
|
-
* Strip [PALMIER_*] marker lines from agent output.
|
|
162
|
-
*/
|
|
163
149
|
export function stripPalmierMarkers(output: string): string {
|
|
164
150
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
165
151
|
}
|
|
166
152
|
|
|
167
|
-
/**
|
|
168
|
-
* Append a conversation message to the RESULT file and notify connected clients.
|
|
169
|
-
*/
|
|
170
153
|
async function appendAndNotify(
|
|
171
154
|
ctx: InvocationContext,
|
|
172
155
|
msg: Parameters<typeof appendRunMessage>[2],
|
|
@@ -175,9 +158,7 @@ async function appendAndNotify(
|
|
|
175
158
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
176
159
|
}
|
|
177
160
|
|
|
178
|
-
/**
|
|
179
|
-
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
180
|
-
*/
|
|
161
|
+
/** The latest run dir with no status messages yet — freshly created by the RPC handler. */
|
|
181
162
|
function findLatestPendingRunId(taskDir: string): string | null {
|
|
182
163
|
const dirs = fs.readdirSync(taskDir)
|
|
183
164
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
@@ -190,8 +171,8 @@ function findLatestPendingRunId(taskDir: string): string | null {
|
|
|
190
171
|
}
|
|
191
172
|
|
|
192
173
|
/**
|
|
193
|
-
* If the RPC handler already wrote "aborted"
|
|
194
|
-
*
|
|
174
|
+
* If the RPC handler already wrote "aborted" (via task.abort), respect that
|
|
175
|
+
* instead of overwriting with the process's own outcome.
|
|
195
176
|
*/
|
|
196
177
|
function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunningState {
|
|
197
178
|
const current = readTaskStatus(taskDir);
|
|
@@ -199,9 +180,6 @@ function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunning
|
|
|
199
180
|
return outcome;
|
|
200
181
|
}
|
|
201
182
|
|
|
202
|
-
/**
|
|
203
|
-
* Execute a task by ID.
|
|
204
|
-
*/
|
|
205
183
|
export async function runCommand(taskId: string): Promise<void> {
|
|
206
184
|
const config = loadConfig();
|
|
207
185
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
@@ -211,7 +189,6 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
211
189
|
let nc: NatsConnection | undefined;
|
|
212
190
|
const taskName = task.frontmatter.name;
|
|
213
191
|
|
|
214
|
-
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
215
192
|
const existingRunId = findLatestPendingRunId(taskDir);
|
|
216
193
|
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
|
|
217
194
|
if (!existingRunId) {
|
|
@@ -231,7 +208,6 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
231
208
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
232
209
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
233
210
|
|
|
234
|
-
// If requires_confirmation, notify clients and wait
|
|
235
211
|
if (task.frontmatter.requires_confirmation) {
|
|
236
212
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
237
213
|
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
@@ -252,7 +228,6 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
252
228
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
253
229
|
}
|
|
254
230
|
|
|
255
|
-
// Shared invocation context
|
|
256
231
|
const guiEnv = getPlatform().getGuiEnv();
|
|
257
232
|
const agent = getAgent(task.frontmatter.agent);
|
|
258
233
|
const ctx: InvocationContext = {
|
|
@@ -261,7 +236,6 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
261
236
|
};
|
|
262
237
|
|
|
263
238
|
if (task.frontmatter.command) {
|
|
264
|
-
// Command-triggered mode
|
|
265
239
|
const result = await runCommandTriggeredMode(ctx);
|
|
266
240
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
267
241
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
@@ -269,14 +243,12 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
269
243
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
270
244
|
} else if (task.frontmatter.schedule_type === "on_new_notification"
|
|
271
245
|
|| task.frontmatter.schedule_type === "on_new_sms") {
|
|
272
|
-
// Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
|
|
273
246
|
const result = await runEventTriggeredMode(ctx);
|
|
274
247
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
275
248
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
276
249
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
277
250
|
console.log(`Task ${taskId} completed (event-triggered).`);
|
|
278
251
|
} else {
|
|
279
|
-
// Standard execution — add user prompt as first message
|
|
280
252
|
await appendAndNotify(ctx, {
|
|
281
253
|
role: "user",
|
|
282
254
|
time: Date.now(),
|
|
@@ -312,11 +284,9 @@ const MAX_LOG_ENTRIES = 1000;
|
|
|
312
284
|
const MAX_LINE_LENGTH = 200_000;
|
|
313
285
|
|
|
314
286
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* invokes the agent CLI with the user's prompt augmented by that line.
|
|
319
|
-
* Processes lines sequentially with a bounded queue.
|
|
287
|
+
* Spawn a long-running shell command and invoke the agent CLI once per stdout
|
|
288
|
+
* line, with the user's prompt augmented by that line. Sequential with a
|
|
289
|
+
* bounded queue.
|
|
320
290
|
*/
|
|
321
291
|
async function runCommandTriggeredMode(
|
|
322
292
|
ctx: InvocationContext,
|
|
@@ -346,7 +316,6 @@ async function runCommandTriggeredMode(
|
|
|
346
316
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
347
317
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
348
318
|
|
|
349
|
-
// Trim log if too large (keep last MAX_LOG_ENTRIES entries)
|
|
350
319
|
try {
|
|
351
320
|
const content = fs.readFileSync(logPath, "utf-8");
|
|
352
321
|
const entries = content.split("\n---\n").filter(Boolean);
|
|
@@ -380,7 +349,7 @@ async function runCommandTriggeredMode(
|
|
|
380
349
|
}
|
|
381
350
|
appendLog(line, "", result.outcome);
|
|
382
351
|
|
|
383
|
-
//
|
|
352
|
+
// Signal "waiting for more input" in the UI.
|
|
384
353
|
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
385
354
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
386
355
|
}
|
|
@@ -422,7 +391,6 @@ async function runCommandTriggeredMode(
|
|
|
422
391
|
process.stderr.write(d);
|
|
423
392
|
});
|
|
424
393
|
|
|
425
|
-
// Wait for command to exit
|
|
426
394
|
const exitCode = await new Promise<number | null>((resolve) => {
|
|
427
395
|
child.on("close", (code: number | null) => {
|
|
428
396
|
commandExited = true;
|
|
@@ -438,7 +406,6 @@ async function runCommandTriggeredMode(
|
|
|
438
406
|
});
|
|
439
407
|
});
|
|
440
408
|
|
|
441
|
-
// Wait for any remaining queued lines to finish processing
|
|
442
409
|
if (lineQueue.length > 0 || processing) {
|
|
443
410
|
await new Promise<void>((resolve) => {
|
|
444
411
|
resolveWhenDone = resolve;
|
|
@@ -464,13 +431,10 @@ async function runCommandTriggeredMode(
|
|
|
464
431
|
}
|
|
465
432
|
|
|
466
433
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* into the user prompt. The run process itself holds no NATS subscription;
|
|
472
|
-
* the daemon handles that and atomically clears the active flag when we see
|
|
473
|
-
* an empty pop, so it can fire up a fresh run on the next incoming event.
|
|
434
|
+
* Drain the daemon-owned per-task event queue via /task-event/pop, invoking
|
|
435
|
+
* the agent once per event. The run process holds no NATS subscription — the
|
|
436
|
+
* daemon owns that and atomically clears the active flag on empty pop so it
|
|
437
|
+
* can fire a fresh run on the next incoming event.
|
|
474
438
|
*/
|
|
475
439
|
async function runEventTriggeredMode(
|
|
476
440
|
ctx: InvocationContext,
|
|
@@ -590,10 +554,6 @@ async function requestConfirmation(
|
|
|
590
554
|
return confirmed;
|
|
591
555
|
}
|
|
592
556
|
|
|
593
|
-
/**
|
|
594
|
-
* Extract report file names from agent output.
|
|
595
|
-
* Looks for lines matching: [PALMIER_REPORT] <filename>
|
|
596
|
-
*/
|
|
597
557
|
const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
|
598
558
|
|
|
599
559
|
export function parseReportFiles(output: string): string[] {
|
|
@@ -602,7 +562,7 @@ export function parseReportFiles(output: string): string[] {
|
|
|
602
562
|
let match;
|
|
603
563
|
while ((match = regex.exec(output)) !== null) {
|
|
604
564
|
const name = match[1].trim();
|
|
605
|
-
// Skip placeholder examples echoed from the prompt (e.g. "<filename>")
|
|
565
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<filename>").
|
|
606
566
|
if (!name || name.startsWith("<")) continue;
|
|
607
567
|
const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
|
|
608
568
|
if (!ALLOWED_REPORT_EXT.includes(ext)) continue;
|
|
@@ -611,17 +571,13 @@ export function parseReportFiles(output: string): string[] {
|
|
|
611
571
|
return files;
|
|
612
572
|
}
|
|
613
573
|
|
|
614
|
-
/**
|
|
615
|
-
* Extract required permissions from agent output.
|
|
616
|
-
* Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
|
|
617
|
-
*/
|
|
618
574
|
export function parsePermissions(output: string): RequiredPermission[] {
|
|
619
575
|
const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
|
|
620
576
|
const perms: RequiredPermission[] = [];
|
|
621
577
|
let match;
|
|
622
578
|
while ((match = regex.exec(output)) !== null) {
|
|
623
579
|
const raw = match[1].trim();
|
|
624
|
-
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
|
|
580
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
|
|
625
581
|
if (raw.startsWith("<")) continue;
|
|
626
582
|
const sep = raw.indexOf("|");
|
|
627
583
|
if (sep !== -1) {
|
|
@@ -633,10 +589,7 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
633
589
|
return perms;
|
|
634
590
|
}
|
|
635
591
|
|
|
636
|
-
/**
|
|
637
|
-
* Parse the agent's output for success/failure markers.
|
|
638
|
-
* Falls back to "finished" if no marker is found.
|
|
639
|
-
*/
|
|
592
|
+
/** Falls back to "finished" if no success/failure marker is found. */
|
|
640
593
|
export function parseTaskOutcome(output: string): TaskRunningState {
|
|
641
594
|
const lastChunk = output.slice(-500);
|
|
642
595
|
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
|