palmier 1.0.8 → 1.0.11
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/README.md +6 -2
- package/dist/agents/agent-instructions.md +8 -4
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.js +17 -26
- package/dist/mcp-tools.d.ts +1 -0
- package/dist/mcp-tools.js +14 -2
- package/dist/pwa/assets/index-CgjL0Axh.js +121 -0
- package/dist/pwa/assets/{index-cjQlPIoo.css → index-DMYhzfFT.css} +1 -1
- package/dist/pwa/assets/{web-BUNx9xzc.js → web-DRfjN6eG.js} +1 -1
- package/dist/pwa/assets/{web-CyIOCae1.js → web-WpTCk2Nv.js} +1 -1
- package/dist/pwa/assets/{web-D4BfJuMP.js → web-zYbQ8b4R.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.js +14 -32
- package/dist/task-files.d.ts +17 -0
- package/dist/task-files.js +113 -0
- package/dist/task.d.ts +1 -2
- package/dist/task.js +1 -19
- package/dist/transports/http-transport.js +4 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/dist/pwa/assets/index-WdXTzNPm.js +0 -120
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://loca
|
|
|
55
55
|
| Tool | Description |
|
|
56
56
|
|------|-------------|
|
|
57
57
|
| `notify` | Send a push notification to the user's device |
|
|
58
|
-
| `request-input` | Request input from the user (blocks until response) |
|
|
59
|
-
| `request-confirmation` | Request confirmation from the user (blocks until response) |
|
|
58
|
+
| `request-input` | Request input from the user (blocks until response; aborting the prompt aborts the task) |
|
|
59
|
+
| `request-confirmation` | Request confirmation from the user (blocks until response; aborting aborts the task) |
|
|
60
60
|
| `fill-password` | Fill a saved password or PIN into the active browser session and submit the form, prompting the user if unknown (the secret is never revealed to the agent) |
|
|
61
61
|
| `device-geolocation` | Get GPS location of the user's mobile device |
|
|
62
62
|
| `read-contacts` | Read the contact list from the user's device |
|
|
@@ -79,6 +79,10 @@ Resources support MCP subscriptions — clients can subscribe via `resources/sub
|
|
|
79
79
|
|
|
80
80
|
All device tools work while the Palmier Android app is in the background — they communicate via FCM data messages which wake the app's service even when it's not in the foreground. Each host has one **linked device**: the phone the host uses for SMS, contacts, location, and other device capabilities. Choose it at pair time (the "Link the host to this device" checkbox) or later from the drawer. Each capability must be enabled and its Android permission granted via toggles in the linked device's drawer.
|
|
81
81
|
|
|
82
|
+
### Files in Agent Output
|
|
83
|
+
|
|
84
|
+
Agents run with the task's run directory (`~/palmier/tasks/<task-id>/<run-id>/`) as their working directory and are instructed to link any file they produce with a relative markdown link, e.g. `[View report](report.md)` or ``. The app opens these links over whichever transport is in use (local, relay, or auto-LAN). It fetches the file through the `task.file` RPC in 512 KiB chunks, so large files fit within the relay's message size. Markdown files render in-app, with their own relative links and images resolved from the file's location. Text and images are previewed, and other file types are offered as a browser download. The host only serves files whose real path (after resolving symlinks) is inside that task's directory, up to 25 MB. A run whose output links an existing file triggers a "report ready" push notification.
|
|
85
|
+
|
|
82
86
|
### Architecture
|
|
83
87
|
|
|
84
88
|
```
|
|
@@ -2,10 +2,14 @@ You are an AI agent executing a task on behalf of the user. Follow these instruc
|
|
|
2
2
|
|
|
3
3
|
All `[PALMIER_*]` markers below are control signals parsed by the host. They MUST be written to **stdout** (not stderr). Markers on stderr are ignored.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
## Linking Files
|
|
6
|
+
|
|
7
|
+
The user reads your output on a different device and can only open files you link. When you refer to a file you created (report, image, data export, etc.):
|
|
8
|
+
- Save it inside the current working directory (subdirectories are fine). Files anywhere else cannot be opened — copy them in first.
|
|
9
|
+
- Link it with a markdown link whose target is a path relative to the current working directory, e.g. `[View report](report.md)` or `[Raw data](exports/results.csv)`. Never use absolute paths or `file://` URLs.
|
|
10
|
+
- Embed images inline with ``.
|
|
11
|
+
- Avoid spaces in file names; if unavoidable, percent-encode them (`my%20report.md`).
|
|
12
|
+
- Inside markdown files you write, link other files relative to that markdown file.
|
|
9
13
|
|
|
10
14
|
## Completion
|
|
11
15
|
|
|
@@ -2,5 +2,4 @@ import type { ParsedTask } from "../types.js";
|
|
|
2
2
|
export declare function getAgentInstructions(task: ParsedTask): string;
|
|
3
3
|
export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
4
4
|
export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
5
|
-
export declare const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
6
5
|
export declare const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
@@ -20,5 +20,4 @@ export function getAgentInstructions(task) {
|
|
|
20
20
|
}
|
|
21
21
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
22
22
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
23
|
-
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
24
23
|
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { TaskRunningState, RequiredPermission } from "../types.js";
|
|
2
2
|
export declare function stripPalmierMarkers(output: string): string;
|
|
3
3
|
export declare function runCommand(taskId: string): Promise<void>;
|
|
4
|
-
|
|
4
|
+
/** Only links to files that exist count, so example links echoed from the prompt are ignored. */
|
|
5
|
+
export declare function listLinkedTaskFiles(taskDir: string, runId: string, output: string): string[];
|
|
5
6
|
export declare function parsePermissions(output: string): RequiredPermission[];
|
|
6
7
|
/** Falls back to "finished" if no success/failure marker is found. */
|
|
7
8
|
export declare function parseTaskOutcome(output: string): TaskRunningState;
|
package/dist/commands/run.js
CHANGED
|
@@ -6,7 +6,8 @@ import { connectNats } from "../nats-client.js";
|
|
|
6
6
|
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir, beginStreamingMessage } from "../task.js";
|
|
7
7
|
import { getAgent } from "../agents/agent.js";
|
|
8
8
|
import { getPlatform } from "../platform/index.js";
|
|
9
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER,
|
|
9
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
10
|
+
import { extractFileLinks, resolveTaskFile } from "../task-files.js";
|
|
10
11
|
import { publishHostEvent } from "../events.js";
|
|
11
12
|
/**
|
|
12
13
|
* Invoke the agent CLI in a continuation loop to handle permission requests.
|
|
@@ -68,7 +69,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
68
69
|
if (notifyTimer)
|
|
69
70
|
clearTimeout(notifyTimer);
|
|
70
71
|
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
71
|
-
const
|
|
72
|
+
const linkedFiles = listLinkedTaskFiles(ctx.taskDir, ctx.runId, result.output);
|
|
72
73
|
const requiredPermissions = parsePermissions(result.output);
|
|
73
74
|
for (const stream of ["stdout", "stderr"]) {
|
|
74
75
|
const trailing = lineBufs[stream];
|
|
@@ -80,19 +81,14 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
80
81
|
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
81
82
|
ensureWriter("stdout").write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
-
ensureWriter("stdout").end(reportFiles);
|
|
85
|
-
}
|
|
86
|
-
else if (writer) {
|
|
87
|
-
writer.end();
|
|
88
|
-
}
|
|
84
|
+
writer?.end();
|
|
89
85
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
90
|
-
if (
|
|
86
|
+
if (linkedFiles.length > 0) {
|
|
91
87
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, {
|
|
92
88
|
event_type: "report-generated",
|
|
93
89
|
run_id: ctx.runId,
|
|
94
90
|
name: ctx.task.frontmatter.name,
|
|
95
|
-
report_files:
|
|
91
|
+
report_files: linkedFiles,
|
|
96
92
|
});
|
|
97
93
|
}
|
|
98
94
|
if (requiredPermissions.length > 0) {
|
|
@@ -357,22 +353,17 @@ async function requestConfirmation(config, task, taskDir) {
|
|
|
357
353
|
});
|
|
358
354
|
return confirmed;
|
|
359
355
|
}
|
|
360
|
-
|
|
361
|
-
export function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!ALLOWED_REPORT_EXT.includes(ext))
|
|
372
|
-
continue;
|
|
373
|
-
files.push(name);
|
|
374
|
-
}
|
|
375
|
-
return files;
|
|
356
|
+
/** Only links to files that exist count, so example links echoed from the prompt are ignored. */
|
|
357
|
+
export function listLinkedTaskFiles(taskDir, runId, output) {
|
|
358
|
+
return extractFileLinks(output).filter((file) => {
|
|
359
|
+
try {
|
|
360
|
+
resolveTaskFile(taskDir, runId, file);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
376
367
|
}
|
|
377
368
|
export function parsePermissions(output) {
|
|
378
369
|
const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface ToolContext {
|
|
|
10
10
|
publishEvent: (id: string, payload: Record<string, unknown>) => Promise<void>;
|
|
11
11
|
sessionId: string;
|
|
12
12
|
agentName?: string;
|
|
13
|
+
abortTask: (taskId: string) => Promise<void>;
|
|
13
14
|
}
|
|
14
15
|
export interface ToolDefinition {
|
|
15
16
|
name: string;
|
package/dist/mcp-tools.js
CHANGED
|
@@ -95,6 +95,7 @@ const requestInputTool = {
|
|
|
95
95
|
event_type: "input-resolved", host_id: ctx.config.hostId,
|
|
96
96
|
session_id: ctx.sessionId, status: "aborted",
|
|
97
97
|
});
|
|
98
|
+
await abortSessionTask(ctx);
|
|
98
99
|
return { aborted: true };
|
|
99
100
|
}
|
|
100
101
|
await ctx.publishEvent("_input", {
|
|
@@ -123,7 +124,7 @@ function recordUserInputToRun(ctx, questions, response, aborted) {
|
|
|
123
124
|
if (!runId)
|
|
124
125
|
return;
|
|
125
126
|
const content = aborted
|
|
126
|
-
? "
|
|
127
|
+
? "Aborted"
|
|
127
128
|
: questions.map((q, i) => `**${q}**\n\n${response[i] ?? ""}`).join("\n\n");
|
|
128
129
|
try {
|
|
129
130
|
spliceUserMessage(taskDir, runId, {
|
|
@@ -138,6 +139,13 @@ function recordUserInputToRun(ctx, questions, response, aborted) {
|
|
|
138
139
|
// Run file missing or unwritable — best-effort, do not fail the tool call.
|
|
139
140
|
}
|
|
140
141
|
}
|
|
142
|
+
/** No-op for pure MCP-protocol sessions, whose sessionId is a random UUID rather than a task. */
|
|
143
|
+
async function abortSessionTask(ctx) {
|
|
144
|
+
const taskId = ctx.sessionId;
|
|
145
|
+
if (!taskId || !fs.existsSync(getTaskDir(ctx.config.projectRoot, taskId)))
|
|
146
|
+
return;
|
|
147
|
+
await ctx.abortTask(taskId);
|
|
148
|
+
}
|
|
141
149
|
const requestConfirmationTool = {
|
|
142
150
|
name: "request-confirmation",
|
|
143
151
|
description: [
|
|
@@ -176,6 +184,8 @@ const requestConfirmationTool = {
|
|
|
176
184
|
session_id: ctx.sessionId,
|
|
177
185
|
status: confirmed ? "confirmed" : "aborted",
|
|
178
186
|
});
|
|
187
|
+
if (!confirmed)
|
|
188
|
+
await abortSessionTask(ctx);
|
|
179
189
|
return { confirmed };
|
|
180
190
|
},
|
|
181
191
|
};
|
|
@@ -231,8 +241,10 @@ const fillPasswordTool = {
|
|
|
231
241
|
session_id: ctx.sessionId, status: aborted ? "aborted" : "provided",
|
|
232
242
|
});
|
|
233
243
|
// Never record the secret to the task run file (unlike request-input).
|
|
234
|
-
if (aborted)
|
|
244
|
+
if (aborted) {
|
|
245
|
+
await abortSessionTask(ctx);
|
|
235
246
|
return { aborted: true };
|
|
247
|
+
}
|
|
236
248
|
password = response[0];
|
|
237
249
|
// The PWA appends a "save"/"nosave" flag for password prompts; persist
|
|
238
250
|
// unless the user opted out via the dialog's checkbox.
|