palmier 0.9.20 → 0.9.22
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 +20 -17
- package/dist/agents/wizard.d.ts +23 -0
- package/dist/agents/wizard.js +166 -0
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +59 -0
- package/dist/commands/init.js +22 -122
- package/dist/commands/run.js +15 -2
- package/dist/index.js +7 -0
- package/dist/pwa/assets/{index-BcEa2BTo.css → index-B9oqDWJv.css} +1 -1
- package/dist/pwa/assets/{index-xF0O5I2z.js → index-DsOLf_o3.js} +3 -3
- package/dist/pwa/assets/{web-Dnq4poOz.js → web-B0XfObWX.js} +1 -1
- package/dist/pwa/assets/{web-okZXRyxh.js → web-BYY9thT9.js} +1 -1
- package/dist/pwa/assets/{web-DVnCcdDQ.js → web-BZCAN0xX.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,28 +6,26 @@
|
|
|
6
6
|
|
|
7
7
|
**Website:** [palmier.me](https://www.palmier.me) | **Web App:** [app.palmier.me](https://app.palmier.me) | **Android App:** [caihongxu/palmier-android](https://github.com/caihongxu/palmier-android)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Free AI agents, right from your phone.**
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Palmier runs AI agents on your computer using the AI subscriptions you already have, then lets you start or schedule tasks, check progress, and respond to requests from your phone. It also gives your agents access to phone-side capabilities — calendar, contacts, notifications, location, SMS, alarms — so they can react to the real world, not just the terminal.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Palmier is free, open source, and requires no account or API key. It runs as a background daemon on your machine and pairs with a mobile-friendly PWA or the Android app (iOS coming soon).
|
|
14
14
|
|
|
15
|
-
## What Palmier
|
|
15
|
+
## What Palmier does
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
* **Your phone becomes an agent remote** — start, schedule, monitor, and respond to agent tasks without being at your computer. On the same network, the Android app connects over LAN automatically for lower latency.
|
|
18
|
+
* **Agent access to your phone data** — give agents access to your phone's location, calendar, contacts, notifications, and SMS. They can also send email on your behalf, send you push notifications, and ask for your input when needed. (Phone capabilities require the Android app.)
|
|
19
|
+
* **Free with your existing AI subscriptions** — Palmier installs/detects agent CLIs and invokes them, so Claude Pro, Gemini, ChatGPT Plus, and [more](https://www.palmier.me/agents) just work. No extra account, no extra API key.
|
|
20
|
+
* **Task scheduling** — run tasks on a schedule, on demand, or in response to events (e.g. when a push notification arrives), using native OS schedulers (systemd, launchd, Task Scheduler).
|
|
21
|
+
* **You stay in control** — agents can only access phone capabilities you enable; approve requests from your phone, or enable yolo mode to auto-approve.
|
|
22
|
+
* **Your agents, your machine** — agents run on your hardware, not ours. Your data stays on your machine. No account required.
|
|
18
23
|
|
|
19
24
|
It is not:
|
|
20
25
|
|
|
21
26
|
* an agent runtime itself
|
|
22
|
-
* a replacement for Claude Code / Codex CLI / Gemini CLI / OpenClaw / Hermes
|
|
23
27
|
* a system for driving your phone UI like a human tapping through apps
|
|
24
28
|
|
|
25
|
-
Instead, Palmier focuses on:
|
|
26
|
-
|
|
27
|
-
* letting agents access phone-side capabilities and context in the background
|
|
28
|
-
* letting you talk to, manage, and schedule your agents from your phone
|
|
29
|
-
* making phone integrations work out of the box without requiring users to wire up separate calendar/email/contact stacks
|
|
30
|
-
|
|
31
29
|
## Quick Start
|
|
32
30
|
|
|
33
31
|
1. Install a supported agent CLI — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Codex CLI](https://github.com/openai/codex), [GitHub Copilot](https://github.com/github/gh-copilot), [OpenClaw](https://openclaw.ai/), or [others](https://www.palmier.me/agents).
|
|
@@ -53,7 +51,7 @@ Instead, Palmier focuses on:
|
|
|
53
51
|
```
|
|
54
52
|
This detects your agents, configures access, installs the background daemon, and starts pairing.
|
|
55
53
|
4. Open `http://localhost:7256` to access the app locally — no pairing needed.
|
|
56
|
-
5. To access from other devices, enter the pairing code shown after init into the [PWA](https://app.palmier.me).
|
|
54
|
+
5. To access from other devices, enter the pairing code shown after init into the [PWA](https://app.palmier.me) or the [Android app](https://github.com/caihongxu/palmier-android/releases/latest/download/palmier.apk).
|
|
57
55
|
|
|
58
56
|
### Prerequisites
|
|
59
57
|
|
|
@@ -132,9 +130,11 @@ Three ways to reach your host, ordered by setup effort:
|
|
|
132
130
|
|
|
133
131
|
| Mode | Where | Pairing | Notes |
|
|
134
132
|
|------|-------|---------|-------|
|
|
135
|
-
| **Local** | `http://localhost:7256` in a browser on the host machine | Not required | Loopback only. No internet needed. |
|
|
136
|
-
| **
|
|
137
|
-
| **
|
|
133
|
+
| **Local (browser)** | `http://localhost:7256` in a browser on the host machine | Not required | Loopback only. No internet or Palmier server connection needed. |
|
|
134
|
+
| **PWA** | [https://app.palmier.me](https://app.palmier.me) in any browser | Required | Installable to your home screen; supports web push notifications. Always goes through the cloud relay. |
|
|
135
|
+
| **Android app** | [Android APK](https://github.com/caihongxu/palmier-android/releases/latest/download/palmier.apk) | Required | Unlocks phone capabilities (GPS, email, calendar, contacts, SMS, alarms), push notifications, and **auto-LAN**. |
|
|
136
|
+
|
|
137
|
+
iOS app coming soon.
|
|
138
138
|
|
|
139
139
|
**Auto-LAN (native app only).** When the Android app is on the same network as the host, it transparently routes RPC over direct LAN HTTP (`http://<host-ip>:7256/rpc/...`) instead of through the relay — lower latency, no protocol change. Browser PWAs can't do this (Private Network Access / mixed-content restrictions) and stay on the relay.
|
|
140
140
|
|
|
@@ -191,10 +191,12 @@ Agents are re-detected on every daemon start; managed-agent versions are re-prob
|
|
|
191
191
|
|
|
192
192
|
### Palmier-managed Agents
|
|
193
193
|
|
|
194
|
-
An agent is considered **Palmier-managed** if it was installed via `palmier init`
|
|
194
|
+
An agent is considered **Palmier-managed** if it was installed via `palmier init`, `palmier agents`, or its update is initiated via the PWA. Palmier-managed agents have a known installed version stamped at install/update time; that version is what the PWA uses to drive the agent soft-update dialog and what's recorded into each session's run metadata so a session always shows the agent version it actually ran with — even after the live agent is upgraded.
|
|
195
195
|
|
|
196
196
|
Agents installed by the user outside the wizard (e.g., `npm install -g <pkg>` directly) are detected and usable but are **not** considered Palmier-managed. The PWA shows them under a separate "Version not managed by Palmier" section and does not offer auto-update for them.
|
|
197
197
|
|
|
198
|
+
Run `palmier agents` to manage agent CLIs after setup: it lists installed agents and offers an interactive picker to install or uninstall one. `palmier init` only prompts for an agent install when none are detected; once any agent is installed, init just lists them and continues with host registration.
|
|
199
|
+
|
|
198
200
|
### Updates
|
|
199
201
|
|
|
200
202
|
- **Palmier itself** — when a newer version of `palmier` is published to npm, the PWA shows a dismissible "Update Available" dialog. Clicking "Update Now" runs `npm update -g palmier` on the host and restarts the daemon. Clicking "Dismiss" suppresses the dialog for that exact version (per host, per device); a future release re-arms it.
|
|
@@ -209,6 +211,7 @@ The default network interface is detected once during `palmier init` and saved t
|
|
|
209
211
|
| Command | Description |
|
|
210
212
|
|---|---|
|
|
211
213
|
| `palmier init` | Interactive setup wizard |
|
|
214
|
+
| `palmier agents` | List, install, and uninstall agent CLIs |
|
|
212
215
|
| `palmier pair` | Generate a pairing code to pair a new device |
|
|
213
216
|
| `palmier clients list` | List active client tokens |
|
|
214
217
|
| `palmier clients revoke <token>` | Revoke a specific client token |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DetectedAgent } from "./agent.js";
|
|
2
|
+
export declare const colors: {
|
|
3
|
+
bold: (s: string) => string;
|
|
4
|
+
dim: (s: string) => string;
|
|
5
|
+
green: (s: string) => string;
|
|
6
|
+
cyan: (s: string) => string;
|
|
7
|
+
red: (s: string) => string;
|
|
8
|
+
yellow: (s: string) => string;
|
|
9
|
+
};
|
|
10
|
+
export declare function printInstalledAgents(agents: DetectedAgent[]): void;
|
|
11
|
+
export interface InstallPickerOptions {
|
|
12
|
+
/** Show a "Cancel" entry as the first choice. */
|
|
13
|
+
allowCancel?: boolean;
|
|
14
|
+
/** Override the picker prompt message. */
|
|
15
|
+
message?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Show the install picker and run the npm install + auth flow for the chosen
|
|
18
|
+
* agent. Returns the new DetectedAgent record on success, or null if the user
|
|
19
|
+
* cancelled, no installables remain, or the install failed. */
|
|
20
|
+
export declare function pickAndInstallAgent(current: DetectedAgent[], options?: InstallPickerOptions): Promise<DetectedAgent | null>;
|
|
21
|
+
/** Show the uninstall picker. Runs `npm uninstall -g` for the chosen agent
|
|
22
|
+
* and returns the agent key on success, or null on cancel/failure/no candidates. */
|
|
23
|
+
export declare function pickAndUninstallAgent(current: DetectedAgent[]): Promise<string | null>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import { selectFromList } from "../prompts.js";
|
|
4
|
+
import { getAgent, getNpmInstalledVersion, listInstallableAgents, } from "./agent.js";
|
|
5
|
+
export const colors = {
|
|
6
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
7
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
8
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
9
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
10
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
11
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
12
|
+
};
|
|
13
|
+
const { bold, dim, green, cyan, red } = colors;
|
|
14
|
+
export function printInstalledAgents(agents) {
|
|
15
|
+
if (agents.length === 0) {
|
|
16
|
+
console.log(` ${dim("(none installed)")}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const a of agents) {
|
|
20
|
+
const version = a.version ? ` ${dim(`v${a.version}`)}` : "";
|
|
21
|
+
const note = a.version ? "" : ` ${dim("(not managed by Palmier)")}`;
|
|
22
|
+
console.log(` ${green("✓")} ${a.label}${version}${note}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Show the install picker and run the npm install + auth flow for the chosen
|
|
26
|
+
* agent. Returns the new DetectedAgent record on success, or null if the user
|
|
27
|
+
* cancelled, no installables remain, or the install failed. */
|
|
28
|
+
export async function pickAndInstallAgent(current, options = {}) {
|
|
29
|
+
const detectedKeys = new Set(current.map((a) => a.key));
|
|
30
|
+
const missing = listInstallableAgents()
|
|
31
|
+
.filter((a) => !detectedKeys.has(a.key))
|
|
32
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
33
|
+
if (missing.length === 0) {
|
|
34
|
+
console.log(`\n${dim("All supported agents are already installed.")}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const installChoices = missing.map((a) => ({
|
|
38
|
+
label: a.freeUsage ? `${a.label} ${green(`[${a.freeUsage}]`)}` : a.label,
|
|
39
|
+
hint: a.npmPackage,
|
|
40
|
+
}));
|
|
41
|
+
const othersChoice = { label: "Others", hint: "see all supported agents" };
|
|
42
|
+
const choices = options.allowCancel
|
|
43
|
+
? [{ label: "Cancel", hint: "go back" }, ...installChoices, othersChoice]
|
|
44
|
+
: [...installChoices, othersChoice];
|
|
45
|
+
const message = options.message ?? `\n${bold("Select an agent to install:")}`;
|
|
46
|
+
const idx = await selectFromList(message, choices);
|
|
47
|
+
if (idx === null)
|
|
48
|
+
return null;
|
|
49
|
+
if (options.allowCancel && idx === 0)
|
|
50
|
+
return null;
|
|
51
|
+
if (idx === choices.length - 1) {
|
|
52
|
+
console.log(`\n${bold("More agents:")} ${cyan("https://www.palmier.me/agents")}`);
|
|
53
|
+
console.log(`Install one with ${cyan("npm install -g <package>")}, then re-run this command.`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const choice = missing[options.allowCancel ? idx - 1 : idx];
|
|
57
|
+
if (!installAgentPackage(choice))
|
|
58
|
+
return null;
|
|
59
|
+
console.log(green(` ${choice.label} installed.`));
|
|
60
|
+
const tool = getAgent(choice.key);
|
|
61
|
+
const version = getNpmInstalledVersion(choice.npmPackage) ?? undefined;
|
|
62
|
+
const record = {
|
|
63
|
+
key: choice.key,
|
|
64
|
+
label: choice.label,
|
|
65
|
+
...(tool.supportsPermissions ? { supportsPermissions: true } : {}),
|
|
66
|
+
...(tool.supportsYolo ? { supportsYolo: true } : {}),
|
|
67
|
+
npmPackage: choice.npmPackage,
|
|
68
|
+
...(version ? { version } : {}),
|
|
69
|
+
};
|
|
70
|
+
if (tool.authArgs && tool.authArgs.length > 0) {
|
|
71
|
+
runAgentAuthFlow(choice.label, tool.command, tool.authArgs);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(`\n${bold("Next: authenticate the CLI.")}`);
|
|
75
|
+
console.log(` Run ${cyan(choice.command)} in another terminal and follow the sign-in prompts.`);
|
|
76
|
+
console.log(` Palmier will use the CLI on your behalf once it's signed in.`);
|
|
77
|
+
}
|
|
78
|
+
await waitForEnter("Press Enter once authentication is complete...");
|
|
79
|
+
return record;
|
|
80
|
+
}
|
|
81
|
+
/** Show the uninstall picker. Runs `npm uninstall -g` for the chosen agent
|
|
82
|
+
* and returns the agent key on success, or null on cancel/failure/no candidates. */
|
|
83
|
+
export async function pickAndUninstallAgent(current) {
|
|
84
|
+
const uninstallable = current.filter((a) => a.npmPackage);
|
|
85
|
+
if (uninstallable.length === 0) {
|
|
86
|
+
console.log(`\n${dim("No agents available to uninstall.")}`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const choices = [
|
|
90
|
+
{ label: "Cancel", hint: "go back" },
|
|
91
|
+
...uninstallable.map((a) => ({
|
|
92
|
+
label: a.label,
|
|
93
|
+
hint: a.npmPackage,
|
|
94
|
+
})),
|
|
95
|
+
];
|
|
96
|
+
const idx = await selectFromList(`\n${bold("Select an agent to uninstall:")}`, choices);
|
|
97
|
+
if (idx === null || idx === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const target = uninstallable[idx - 1];
|
|
100
|
+
if (!target.npmPackage)
|
|
101
|
+
return null;
|
|
102
|
+
if (!uninstallAgentPackage(target.npmPackage))
|
|
103
|
+
return null;
|
|
104
|
+
console.log(green(` ${target.label} uninstalled.`));
|
|
105
|
+
return target.key;
|
|
106
|
+
}
|
|
107
|
+
function installAgentPackage(agent) {
|
|
108
|
+
console.log(`\nInstalling ${cyan(agent.npmPackage)}...\n`);
|
|
109
|
+
const cmd = `npm install -g ${agent.npmPackage}`;
|
|
110
|
+
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
|
|
111
|
+
if (result.error) {
|
|
112
|
+
console.log(`\n${red(`Failed to run npm: ${result.error.message}`)}`);
|
|
113
|
+
console.log(`Make sure ${cyan("npm")} is on your PATH, then retry.`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (result.status !== 0) {
|
|
117
|
+
const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
118
|
+
console.log(`\n${red(`${cmd} failed (${exitInfo}).`)}`);
|
|
119
|
+
if (process.platform === "win32") {
|
|
120
|
+
console.log(`If this is a permissions error, try opening a terminal as Administrator and re-running.`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(`If this is a permissions error, try running with ${cyan("sudo")} or fix your global npm prefix.`);
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function uninstallAgentPackage(npmPackage) {
|
|
130
|
+
console.log(`\nUninstalling ${cyan(npmPackage)}...\n`);
|
|
131
|
+
const cmd = `npm uninstall -g ${npmPackage}`;
|
|
132
|
+
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
|
|
133
|
+
if (result.error) {
|
|
134
|
+
console.log(`\n${red(`Failed to run npm: ${result.error.message}`)}`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (result.status !== 0) {
|
|
138
|
+
const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
139
|
+
console.log(`\n${red(`${cmd} failed (${exitInfo}).`)}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function runAgentAuthFlow(label, command, args) {
|
|
145
|
+
const cmd = `${command} ${args.join(" ")}`;
|
|
146
|
+
console.log(`\n${bold(`Authenticating ${label}...`)} ${dim(`(${cmd})`)}\n`);
|
|
147
|
+
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
|
|
148
|
+
console.log("");
|
|
149
|
+
if (result.error) {
|
|
150
|
+
console.log(red(`Auth failed: could not run ${cmd} — ${result.error.message}`));
|
|
151
|
+
console.log(`Re-run ${cyan(cmd)} manually after this.\n`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
156
|
+
console.log(red(`Auth failed (${exitInfo}).`));
|
|
157
|
+
console.log(`Re-run ${cyan(cmd)} manually after this.\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(green(`Successfully authenticated ${label}.\n`));
|
|
161
|
+
}
|
|
162
|
+
async function waitForEnter(message) {
|
|
163
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
164
|
+
await new Promise((resolve) => rl.question(`\n${dim(message)} `, resolve));
|
|
165
|
+
rl.close();
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function agentsCommand(): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
2
|
+
import { detectAgents } from "../agents/agent.js";
|
|
3
|
+
import { colors, pickAndInstallAgent, pickAndUninstallAgent, printInstalledAgents, } from "../agents/wizard.js";
|
|
4
|
+
import { selectFromList } from "../prompts.js";
|
|
5
|
+
import { getPlatform } from "../platform/index.js";
|
|
6
|
+
const { bold, dim, cyan } = colors;
|
|
7
|
+
export async function agentsCommand() {
|
|
8
|
+
let config = null;
|
|
9
|
+
try {
|
|
10
|
+
config = loadConfig();
|
|
11
|
+
}
|
|
12
|
+
catch { /* host not yet initialized */ }
|
|
13
|
+
let agents = await detectAgents(config?.agents);
|
|
14
|
+
let dirty = false;
|
|
15
|
+
while (true) {
|
|
16
|
+
console.log(`\n${bold("=== Installed agents ===")}\n`);
|
|
17
|
+
printInstalledAgents(agents);
|
|
18
|
+
const idx = await selectFromList(`\n${bold("What would you like to do?")}`, [
|
|
19
|
+
{ label: "Install an agent", hint: "add a supported CLI" },
|
|
20
|
+
{ label: "Uninstall an agent", hint: "remove a supported CLI" },
|
|
21
|
+
{ label: "Done", hint: "exit" },
|
|
22
|
+
]);
|
|
23
|
+
if (idx === null || idx === 2)
|
|
24
|
+
break;
|
|
25
|
+
if (idx === 0) {
|
|
26
|
+
const installed = await pickAndInstallAgent(agents, { allowCancel: true });
|
|
27
|
+
if (installed) {
|
|
28
|
+
agents = [...agents.filter((a) => a.key !== installed.key), installed];
|
|
29
|
+
dirty = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (idx === 1) {
|
|
33
|
+
const removedKey = await pickAndUninstallAgent(agents);
|
|
34
|
+
if (removedKey) {
|
|
35
|
+
agents = agents.filter((a) => a.key !== removedKey);
|
|
36
|
+
dirty = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
agents = await detectAgents(agents);
|
|
40
|
+
}
|
|
41
|
+
if (!dirty)
|
|
42
|
+
return;
|
|
43
|
+
if (config) {
|
|
44
|
+
config.agents = agents;
|
|
45
|
+
saveConfig(config);
|
|
46
|
+
console.log(`\nConfig saved to ${dim("~/.config/palmier/host.json")}`);
|
|
47
|
+
try {
|
|
48
|
+
await getPlatform().restartDaemon();
|
|
49
|
+
console.log(dim("Daemon restarted."));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
console.log(dim(`(Daemon restart skipped: ${message})`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(`\n${dim(`Run ${cyan("palmier init")} to register this host with the new agent set.`)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import { spawnSync } from "child_process";
|
|
3
2
|
import { loadConfig, saveConfig } from "../config.js";
|
|
4
|
-
import { detectAgents
|
|
3
|
+
import { detectAgents } from "../agents/agent.js";
|
|
4
|
+
import { colors, pickAndInstallAgent, printInstalledAgents } from "../agents/wizard.js";
|
|
5
5
|
import { getPlatform } from "../platform/index.js";
|
|
6
6
|
import { pairCommand } from "./pair.js";
|
|
7
7
|
import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
|
|
8
8
|
import { listTasks } from "../task.js";
|
|
9
|
-
|
|
10
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
11
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
12
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
13
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
14
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
9
|
+
const { bold, dim, green, cyan, red } = colors;
|
|
15
10
|
export async function initCommand() {
|
|
16
11
|
console.log(`\n${bold("=== Palmier Host Setup ===")}\n`);
|
|
17
12
|
console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
|
|
@@ -23,18 +18,26 @@ export async function initCommand() {
|
|
|
23
18
|
}
|
|
24
19
|
catch { /* first init */ }
|
|
25
20
|
let agents = await detectAgents(previousConfig?.agents);
|
|
26
|
-
logDetectedAgents(agents);
|
|
27
|
-
await offerAgentInstall(agents, () => {
|
|
28
|
-
if (previousConfig) {
|
|
29
|
-
previousConfig.agents = agents;
|
|
30
|
-
saveConfig(previousConfig);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
21
|
if (agents.length === 0) {
|
|
34
|
-
console.log(`\n${red("No agent CLIs detected.")} Palmier
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
console.log(`\n${red("No agent CLIs detected.")} Palmier needs at least one to run.`);
|
|
23
|
+
const installed = await pickAndInstallAgent(agents);
|
|
24
|
+
if (installed) {
|
|
25
|
+
agents = [...agents, installed];
|
|
26
|
+
if (previousConfig) {
|
|
27
|
+
previousConfig.agents = agents;
|
|
28
|
+
saveConfig(previousConfig);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (agents.length === 0) {
|
|
32
|
+
console.log(`\nSee supported agents: https://www.palmier.me/agents\n`);
|
|
33
|
+
console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(`\n${bold("Installed agents:")}`);
|
|
39
|
+
printInstalledAgents(agents);
|
|
40
|
+
console.log(`\n${dim(`Tip: run ${cyan("palmier agents")} to install more or uninstall.\n`)}`);
|
|
38
41
|
}
|
|
39
42
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
40
43
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
@@ -129,109 +132,6 @@ export async function initCommand() {
|
|
|
129
132
|
throw err;
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
|
-
async function offerAgentInstall(agents, onAgentInstalled) {
|
|
133
|
-
while (true) {
|
|
134
|
-
const detectedKeys = new Set(agents.map((a) => a.key));
|
|
135
|
-
const missing = listInstallableAgents()
|
|
136
|
-
.filter((a) => !detectedKeys.has(a.key))
|
|
137
|
-
.sort((a, b) => a.label.localeCompare(b.label));
|
|
138
|
-
if (missing.length === 0)
|
|
139
|
-
return;
|
|
140
|
-
const hasAgents = agents.length > 0;
|
|
141
|
-
const message = hasAgents
|
|
142
|
-
? `\n${bold("Install additional agents?")} The following supported agents can be installed:`
|
|
143
|
-
: `\n${red("No agent CLIs detected.")} Palmier can install one for you via npm:`;
|
|
144
|
-
const installChoices = missing.map((a) => ({
|
|
145
|
-
label: a.freeUsage ? `${a.label} ${green(`[${a.freeUsage}]`)}` : a.label,
|
|
146
|
-
hint: `${a.npmPackage}`,
|
|
147
|
-
}));
|
|
148
|
-
const choices = hasAgents
|
|
149
|
-
? [{ label: "No — continue to the next step ", hint: "skip installation" }, ...installChoices]
|
|
150
|
-
: installChoices;
|
|
151
|
-
const idx = await selectFromList(message, choices);
|
|
152
|
-
if (idx === null)
|
|
153
|
-
return;
|
|
154
|
-
if (hasAgents && idx === 0)
|
|
155
|
-
return;
|
|
156
|
-
const choice = missing[hasAgents ? idx - 1 : idx];
|
|
157
|
-
if (!installAgentPackage(choice))
|
|
158
|
-
return;
|
|
159
|
-
console.log(green(` ${choice.label} installed.`));
|
|
160
|
-
// Stamp the agent record with version *before* auth so that an interrupted
|
|
161
|
-
// wizard (Ctrl+C during sign-in, etc.) still leaves the agent recorded as
|
|
162
|
-
// Palmier-managed in the persisted config on next run.
|
|
163
|
-
const tool = getAgent(choice.key);
|
|
164
|
-
const version = getNpmInstalledVersion(choice.npmPackage) ?? undefined;
|
|
165
|
-
agents.push({
|
|
166
|
-
key: choice.key,
|
|
167
|
-
label: choice.label,
|
|
168
|
-
...(tool.supportsPermissions ? { supportsPermissions: true } : {}),
|
|
169
|
-
...(tool.supportsYolo ? { supportsYolo: true } : {}),
|
|
170
|
-
npmPackage: choice.npmPackage,
|
|
171
|
-
...(version ? { version } : {}),
|
|
172
|
-
});
|
|
173
|
-
onAgentInstalled?.();
|
|
174
|
-
if (tool.authArgs && tool.authArgs.length > 0) {
|
|
175
|
-
runAgentAuthFlow(choice.label, tool.command, tool.authArgs);
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
console.log(`\n${bold("Next: authenticate the CLI.")}`);
|
|
179
|
-
console.log(` Run ${cyan(choice.command)} in another terminal and follow the sign-in prompts.`);
|
|
180
|
-
console.log(` Palmier will use the CLI on your behalf once it's signed in.`);
|
|
181
|
-
}
|
|
182
|
-
await waitForEnter("Press Enter once authentication is complete...");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async function waitForEnter(message) {
|
|
186
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
187
|
-
await new Promise((resolve) => rl.question(`\n${dim(message)} `, resolve));
|
|
188
|
-
rl.close();
|
|
189
|
-
}
|
|
190
|
-
function logDetectedAgents(agents) {
|
|
191
|
-
if (agents.length === 0)
|
|
192
|
-
return;
|
|
193
|
-
console.log(` Found: ${green(agents.map((a) => a.version ? `${a.label} v${a.version}` : a.label).join(", "))}`);
|
|
194
|
-
}
|
|
195
|
-
function runAgentAuthFlow(label, command, args) {
|
|
196
|
-
const cmd = `${command} ${args.join(" ")}`;
|
|
197
|
-
console.log(`\n${bold(`Authenticating ${label}...`)} ${dim(`(${cmd})`)}\n`);
|
|
198
|
-
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
|
|
199
|
-
console.log("");
|
|
200
|
-
if (result.error) {
|
|
201
|
-
console.log(red(`Auth failed: could not run ${cmd} — ${result.error.message}`));
|
|
202
|
-
console.log(`Re-run ${cyan(cmd)} manually after init finishes.\n`);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (result.status !== 0) {
|
|
206
|
-
const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
207
|
-
console.log(red(`Auth failed (${exitInfo}).`));
|
|
208
|
-
console.log(`Re-run ${cyan(cmd)} manually after init finishes.\n`);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
console.log(green(`Successfully authenticated ${label}.\n`));
|
|
212
|
-
}
|
|
213
|
-
function installAgentPackage(agent) {
|
|
214
|
-
console.log(`\nInstalling ${cyan(agent.npmPackage)}...\n`);
|
|
215
|
-
const cmd = `npm install -g ${agent.npmPackage}`;
|
|
216
|
-
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
|
|
217
|
-
if (result.error) {
|
|
218
|
-
console.log(`\n${red(`Failed to run npm: ${result.error.message}`)}`);
|
|
219
|
-
console.log(`Make sure ${cyan("npm")} is on your PATH, then retry.`);
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
if (result.status !== 0) {
|
|
223
|
-
const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
224
|
-
console.log(`\n${red(`${cmd} failed (${exitInfo}).`)}`);
|
|
225
|
-
if (process.platform === "win32") {
|
|
226
|
-
console.log(`If this is a permissions error, try opening a terminal as Administrator and re-running ${cyan("palmier init")}.`);
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
console.log(`If this is a permissions error, try running with ${cyan("sudo")} or fix your global npm prefix.`);
|
|
230
|
-
}
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
135
|
async function registerHost(serverUrl, existingHostId) {
|
|
236
136
|
try {
|
|
237
137
|
const res = await fetch(`${serverUrl}/api/hosts/register`, {
|
package/dist/commands/run.js
CHANGED
|
@@ -227,8 +227,21 @@ export async function runCommand(taskId) {
|
|
|
227
227
|
transientPermissions: [],
|
|
228
228
|
};
|
|
229
229
|
if (task.frontmatter.command) {
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
let outcome;
|
|
231
|
+
// Command-triggered tasks auto-restart when the underlying command exits
|
|
232
|
+
// on its own — only a user abort breaks the loop.
|
|
233
|
+
while (true) {
|
|
234
|
+
const result = await runCommandTriggeredMode(ctx);
|
|
235
|
+
outcome = resolveOutcome(taskDir, result.outcome);
|
|
236
|
+
if (outcome === "aborted")
|
|
237
|
+
break;
|
|
238
|
+
console.log(`Task ${taskId} command exited (${outcome}); auto-restarting.`);
|
|
239
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
240
|
+
if (resolveOutcome(taskDir, "finished") === "aborted") {
|
|
241
|
+
outcome = "aborted";
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
232
245
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
233
246
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
234
247
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { serveCommand } from "./commands/serve.js";
|
|
|
11
11
|
import { pairCommand } from "./commands/pair.js";
|
|
12
12
|
import { restartCommand } from "./commands/restart.js";
|
|
13
13
|
import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
|
|
14
|
+
import { agentsCommand } from "./commands/agents.js";
|
|
14
15
|
import { uninstallCommand } from "./commands/uninstall.js";
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
@@ -76,6 +77,12 @@ clientsCmd
|
|
|
76
77
|
.action(async () => {
|
|
77
78
|
await clientsRevokeAllCommand();
|
|
78
79
|
});
|
|
80
|
+
program
|
|
81
|
+
.command("agents")
|
|
82
|
+
.description("List, install, and uninstall agent CLIs")
|
|
83
|
+
.action(async () => {
|
|
84
|
+
await agentsCommand();
|
|
85
|
+
});
|
|
79
86
|
program
|
|
80
87
|
.command("uninstall")
|
|
81
88
|
.description("Stop the daemon and remove all scheduled tasks")
|