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 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
- You already have AI agents running on your machine. Palmier is an agent-agnostic bridge between those agents and your phone.
9
+ **Free AI agents, right from your phone.**
10
10
 
11
- From your phone, you can start sessions, schedule tasks, approve requests, and review results. From your machine, your agents can use phone-side capabilities like notifications, location, SMS, contacts, and calendar — so they can react to the real world, not just the terminal.
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
- It runs on your machine as a background daemon and pairs with a mobile-friendly PWA.
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 is
15
+ ## What Palmier does
16
16
 
17
- Palmier is an **agent-agnostic phone bridge and mobile control layer** for the agents you already use.
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
- | **Remote (web)** | [https://app.palmier.me](https://app.palmier.me) in any browser | Required | Always goes through the cloud relay. |
137
- | **Remote (app)** | [Android APK](https://github.com/caihongxu/palmier-android/releases/latest/download/palmier.apk) | Required | Push notifications, device capabilities, and **auto-LAN**. |
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` (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.
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
+ }
@@ -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, getAgent, getNpmInstalledVersion, listInstallableAgents } from "../agents/agent.js";
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
- import { selectFromList } from "../prompts.js";
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 requires at least one supported agent CLI.\n`);
35
- console.log(`See supported agents: https://www.palmier.me/agents\n`);
36
- console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
37
- process.exit(1);
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`, {
@@ -227,8 +227,21 @@ export async function runCommand(taskId) {
227
227
  transientPermissions: [],
228
228
  };
229
229
  if (task.frontmatter.command) {
230
- const result = await runCommandTriggeredMode(ctx);
231
- const outcome = resolveOutcome(taskDir, result.outcome);
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")