palmier 0.9.20 → 0.9.21

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
@@ -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,160 @@
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 choices = options.allowCancel
42
+ ? [{ label: "Cancel", hint: "go back" }, ...installChoices]
43
+ : installChoices;
44
+ const message = options.message ?? `\n${bold("Select an agent to install:")}`;
45
+ const idx = await selectFromList(message, choices);
46
+ if (idx === null)
47
+ return null;
48
+ if (options.allowCancel && idx === 0)
49
+ return null;
50
+ const choice = missing[options.allowCancel ? idx - 1 : idx];
51
+ if (!installAgentPackage(choice))
52
+ return null;
53
+ console.log(green(` ${choice.label} installed.`));
54
+ const tool = getAgent(choice.key);
55
+ const version = getNpmInstalledVersion(choice.npmPackage) ?? undefined;
56
+ const record = {
57
+ key: choice.key,
58
+ label: choice.label,
59
+ ...(tool.supportsPermissions ? { supportsPermissions: true } : {}),
60
+ ...(tool.supportsYolo ? { supportsYolo: true } : {}),
61
+ npmPackage: choice.npmPackage,
62
+ ...(version ? { version } : {}),
63
+ };
64
+ if (tool.authArgs && tool.authArgs.length > 0) {
65
+ runAgentAuthFlow(choice.label, tool.command, tool.authArgs);
66
+ }
67
+ else {
68
+ console.log(`\n${bold("Next: authenticate the CLI.")}`);
69
+ console.log(` Run ${cyan(choice.command)} in another terminal and follow the sign-in prompts.`);
70
+ console.log(` Palmier will use the CLI on your behalf once it's signed in.`);
71
+ }
72
+ await waitForEnter("Press Enter once authentication is complete...");
73
+ return record;
74
+ }
75
+ /** Show the uninstall picker. Runs `npm uninstall -g` for the chosen agent
76
+ * and returns the agent key on success, or null on cancel/failure/no candidates. */
77
+ export async function pickAndUninstallAgent(current) {
78
+ const uninstallable = current.filter((a) => a.npmPackage);
79
+ if (uninstallable.length === 0) {
80
+ console.log(`\n${dim("No agents available to uninstall.")}`);
81
+ return null;
82
+ }
83
+ const choices = [
84
+ { label: "Cancel", hint: "go back" },
85
+ ...uninstallable.map((a) => ({
86
+ label: a.label,
87
+ hint: a.npmPackage,
88
+ })),
89
+ ];
90
+ const idx = await selectFromList(`\n${bold("Select an agent to uninstall:")}`, choices);
91
+ if (idx === null || idx === 0)
92
+ return null;
93
+ const target = uninstallable[idx - 1];
94
+ if (!target.npmPackage)
95
+ return null;
96
+ if (!uninstallAgentPackage(target.npmPackage))
97
+ return null;
98
+ console.log(green(` ${target.label} uninstalled.`));
99
+ return target.key;
100
+ }
101
+ function installAgentPackage(agent) {
102
+ console.log(`\nInstalling ${cyan(agent.npmPackage)}...\n`);
103
+ const cmd = `npm install -g ${agent.npmPackage}`;
104
+ const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
105
+ if (result.error) {
106
+ console.log(`\n${red(`Failed to run npm: ${result.error.message}`)}`);
107
+ console.log(`Make sure ${cyan("npm")} is on your PATH, then retry.`);
108
+ return false;
109
+ }
110
+ if (result.status !== 0) {
111
+ const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
112
+ console.log(`\n${red(`${cmd} failed (${exitInfo}).`)}`);
113
+ if (process.platform === "win32") {
114
+ console.log(`If this is a permissions error, try opening a terminal as Administrator and re-running.`);
115
+ }
116
+ else {
117
+ console.log(`If this is a permissions error, try running with ${cyan("sudo")} or fix your global npm prefix.`);
118
+ }
119
+ return false;
120
+ }
121
+ return true;
122
+ }
123
+ function uninstallAgentPackage(npmPackage) {
124
+ console.log(`\nUninstalling ${cyan(npmPackage)}...\n`);
125
+ const cmd = `npm uninstall -g ${npmPackage}`;
126
+ const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
127
+ if (result.error) {
128
+ console.log(`\n${red(`Failed to run npm: ${result.error.message}`)}`);
129
+ return false;
130
+ }
131
+ if (result.status !== 0) {
132
+ const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
133
+ console.log(`\n${red(`${cmd} failed (${exitInfo}).`)}`);
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ function runAgentAuthFlow(label, command, args) {
139
+ const cmd = `${command} ${args.join(" ")}`;
140
+ console.log(`\n${bold(`Authenticating ${label}...`)} ${dim(`(${cmd})`)}\n`);
141
+ const result = spawnSync(cmd, { shell: true, stdio: "inherit" });
142
+ console.log("");
143
+ if (result.error) {
144
+ console.log(red(`Auth failed: could not run ${cmd} — ${result.error.message}`));
145
+ console.log(`Re-run ${cyan(cmd)} manually after this.\n`);
146
+ return;
147
+ }
148
+ if (result.status !== 0) {
149
+ const exitInfo = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
150
+ console.log(red(`Auth failed (${exitInfo}).`));
151
+ console.log(`Re-run ${cyan(cmd)} manually after this.\n`);
152
+ return;
153
+ }
154
+ console.log(green(`Successfully authenticated ${label}.\n`));
155
+ }
156
+ async function waitForEnter(message) {
157
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
158
+ await new Promise((resolve) => rl.question(`\n${dim(message)} `, resolve));
159
+ rl.close();
160
+ }
@@ -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`, {
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")