palmier 0.9.19 → 0.9.20

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
@@ -67,7 +67,7 @@ Palmier runs as a background daemon (systemd on Linux, launchd on macOS, Task Sc
67
67
 
68
68
  ### MCP Server
69
69
 
70
- Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://localhost:7256/mcp` (streamable HTTP transport). MCP-capable agents can register it to get tool and resource definitions automatically. The same tools and resources are also available as REST endpoints for curl-based agents.
70
+ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://localhost:7256/mcp` (streamable HTTP transport). MCP-capable agents can register it to get tool and resource definitions automatically. The same tools and resources are also readily available if running agent tasks directly from Palmier app, without installing the MCP server.
71
71
 
72
72
  **MCP server URL:** `http://localhost:7256/mcp`
73
73
 
@@ -174,19 +174,31 @@ Revoking the linked device also clears the host's linked-device record; device c
174
174
  ### The `init` Command
175
175
 
176
176
  The wizard:
177
- - Detects installed agent CLIs and caches the result
177
+ - Detects installed agent CLIs and caches the result; agents previously installed by Palmier have their installed version re-probed so the recorded version stays in sync with manual upgrades
178
+ - Offers to install missing supported agents from npm (one at a time, arrow-key menu). After each install the agent's version is stamped (becoming "Palmier-managed"), the wizard kicks off authentication, and waits for you to press Enter before offering the next install. On re-init, the agents list is saved after every install so an interrupted wizard is resumable.
178
179
  - Asks for the HTTP port
179
180
  - Detects the default network interface (used for auto-LAN)
180
181
  - Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
181
182
  - Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
182
- - Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows)
183
+ - Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows). On re-init, also restarts the daemon so the running process picks up the new agents/versions for `host.info`.
183
184
  - Auto-enters pair mode to connect your first device
184
185
 
185
186
  The daemon automatically recovers existing tasks by reinstalling their system timers on startup.
186
187
 
187
188
  > **macOS note:** Palmier installs as a user-level LaunchAgent, so it runs without `sudo`. LaunchAgents only run while the user is logged into the GUI session — after a reboot, scheduled tasks stay dormant until you log in at least once. Enable auto-login in System Settings → Users & Groups if you need unattended operation across reboots.
188
189
 
189
- Agents are re-detected on every daemon start. Run `palmier restart` after installing or removing a CLI.
190
+ Agents are re-detected on every daemon start; managed-agent versions are re-probed live so upgrades performed outside the wizard show up automatically.
191
+
192
+ ### Palmier-managed Agents
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.
195
+
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
+
198
+ ### Updates
199
+
200
+ - **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.
201
+ - **Palmier-managed agents** — same flow per agent: when npm publishes a newer version, the PWA shows an "Agent Update Available" dialog. Clicking "Update Now" runs `npm update -g <pkg>` on the host (no daemon restart needed). Dismissals are per host, per agent, per version.
190
202
 
191
203
  ### Re-detecting the LAN Network
192
204
 
@@ -12,12 +12,19 @@ export interface CommandLine {
12
12
  }>;
13
13
  }
14
14
  export interface AgentTool {
15
+ /** Human-readable name shown in the PWA and CLI (e.g. "Claude Code"). */
16
+ label: string;
15
17
  /** The agent's CLI binary name (e.g. "claude", "kiro-cli"). */
16
18
  command: string;
17
19
  /** Static args for a short, non-interactive prompt. The prompt is appended to the end of this list. */
18
- promptCommandLineArgs: string[];
20
+ promptArgs: string[];
19
21
  /** Single arg passed to `command` to probe whether the CLI is installed. Usually `"--version"`. */
20
- versionCommandLineArg: string;
22
+ probeArg: string;
23
+ /** Optional args to launch the agent's auth flow after a successful install. When set,
24
+ * `palmier init` runs `<command> <args...>` interactively (stdio: "inherit") so the user
25
+ * can sign in before configuration continues. Leave undefined for agents that auth on
26
+ * first run with no separate command. */
27
+ authArgs?: string[];
21
28
  /** Whether this agent supports permission overrides (e.g. --allowedTools).
22
29
  * When falsy, the permissions section is omitted from agent instructions. */
23
30
  supportsPermissions?: boolean;
@@ -59,5 +66,12 @@ export interface InstallableAgent {
59
66
  freeUsage?: string;
60
67
  }
61
68
  export declare function listInstallableAgents(): InstallableAgent[];
62
- export declare function detectAgents(previous?: DetectedAgent[]): Promise<DetectedAgent[]>;
69
+ /** Detect agents present on PATH and resolve their version when they are
70
+ * Palmier-managed. An agent is treated as managed if either:
71
+ * - it had a `version` in the `previous` list (preserved across daemon restarts), or
72
+ * - its key is in `newlyInstalled` (e.g. just installed by the wizard this session).
73
+ *
74
+ * Every managed agent has its version probed live via `npm ls -g`, so manual
75
+ * upgrades outside Palmier are picked up on the next detection. */
76
+ export declare function detectAgents(previous?: DetectedAgent[], newlyInstalled?: Set<string>): Promise<DetectedAgent[]>;
63
77
  export declare function getAgent(name: string): AgentTool;
@@ -18,10 +18,10 @@ import { clineAgent } from "./cline.js";
18
18
  import { qoderAgent } from "./qoder.js";
19
19
  import { hermesAgent } from "./hermes.js";
20
20
  export function getPromptCommandLine(agent, prompt) {
21
- return { args: [...agent.promptCommandLineArgs, prompt] };
21
+ return { args: [...agent.promptArgs, prompt] };
22
22
  }
23
23
  export async function probeAgent(agent) {
24
- const probe = `${agent.command} ${agent.versionCommandLineArg}`;
24
+ const probe = `${agent.command} ${agent.probeArg}`;
25
25
  try {
26
26
  execSync(probe, { stdio: "ignore", shell: SHELL });
27
27
  }
@@ -73,25 +73,6 @@ const agentRegistry = {
73
73
  qoder: qoderAgent,
74
74
  hermes: hermesAgent,
75
75
  };
76
- const agentLabels = {
77
- claude: "Claude Code",
78
- gemini: "Gemini CLI",
79
- codex: "Codex CLI",
80
- droid: "Droid CLI",
81
- openclaw: "OpenClaw",
82
- copilot: "Copilot CLI",
83
- qwen: "Qwen Code",
84
- kimi: "Kimi Code",
85
- goose: "Goose CLI",
86
- opencode: "OpenCode",
87
- deepagents: "Deep Agents CLI",
88
- aider: "Aider",
89
- cursor: "Cursor CLI",
90
- kiro: "Kiro CLI",
91
- cline: "Cline CLI",
92
- qoder: "Qoder CLI",
93
- hermes: "Hermes Agent",
94
- };
95
76
  export function listInstallableAgents() {
96
77
  const out = [];
97
78
  for (const [key, agent] of Object.entries(agentRegistry)) {
@@ -99,7 +80,7 @@ export function listInstallableAgents() {
99
80
  continue;
100
81
  out.push({
101
82
  key,
102
- label: agentLabels[key] ?? key,
83
+ label: agent.label,
103
84
  npmPackage: agent.npmPackage,
104
85
  command: agent.command,
105
86
  ...(agent.freeUsage ? { freeUsage: agent.freeUsage } : {}),
@@ -107,22 +88,31 @@ export function listInstallableAgents() {
107
88
  }
108
89
  return out;
109
90
  }
110
- export async function detectAgents(previous) {
91
+ /** Detect agents present on PATH and resolve their version when they are
92
+ * Palmier-managed. An agent is treated as managed if either:
93
+ * - it had a `version` in the `previous` list (preserved across daemon restarts), or
94
+ * - its key is in `newlyInstalled` (e.g. just installed by the wizard this session).
95
+ *
96
+ * Every managed agent has its version probed live via `npm ls -g`, so manual
97
+ * upgrades outside Palmier are picked up on the next detection. */
98
+ export async function detectAgents(previous, newlyInstalled) {
111
99
  const previousByKey = new Map((previous ?? []).map((a) => [a.key, a]));
112
100
  const detected = [];
113
101
  for (const [key, agent] of Object.entries(agentRegistry)) {
114
- const label = agentLabels[key] ?? key;
115
102
  const ok = await probeAgent(agent);
116
103
  if (!ok)
117
104
  continue;
118
- const prevVersion = previousByKey.get(key)?.version;
105
+ const wasManaged = !!previousByKey.get(key)?.version || (newlyInstalled?.has(key) ?? false);
106
+ const version = wasManaged && agent.npmPackage
107
+ ? getNpmInstalledVersion(agent.npmPackage) ?? undefined
108
+ : undefined;
119
109
  detected.push({
120
110
  key,
121
- label,
111
+ label: agent.label,
122
112
  supportsPermissions: agent.supportsPermissions,
123
113
  supportsYolo: agent.supportsYolo,
124
114
  ...(agent.npmPackage ? { npmPackage: agent.npmPackage } : {}),
125
- ...(prevVersion ? { version: prevVersion } : {}),
115
+ ...(version ? { version } : {}),
126
116
  });
127
117
  }
128
118
  return detected;
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const aiderAgent = {
3
+ label: "Aider",
3
4
  command: "aider",
4
- promptCommandLineArgs: ["--message"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["--message"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,10 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const claudeAgent = {
3
+ label: "Claude Code",
3
4
  command: "claude",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["-p"],
6
+ probeArg: "--version",
7
+ authArgs: ["auth", "login"],
6
8
  supportsPermissions: true,
7
9
  supportsYolo: true,
8
10
  npmPackage: "@anthropic-ai/claude-code",
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const clineAgent = {
3
+ label: "Cline CLI",
3
4
  command: "cline",
4
- promptCommandLineArgs: ["--yolo", "-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["--yolo", "-p"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  npmPackage: "cline",
8
9
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
@@ -1,8 +1,10 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const codexAgent = {
3
+ label: "Codex CLI",
3
4
  command: "codex",
4
- promptCommandLineArgs: ["exec", "--skip-git-repo-check"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["exec", "--skip-git-repo-check"],
6
+ probeArg: "--version",
7
+ authArgs: ["login"],
6
8
  supportsYolo: true,
7
9
  suppressStdErr: true,
8
10
  npmPackage: "@openai/codex",
@@ -1,8 +1,10 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const copilotAgent = {
3
+ label: "Copilot CLI",
3
4
  command: "copilot",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "-v",
5
+ promptArgs: ["-p"],
6
+ probeArg: "-v",
7
+ authArgs: ["login"],
6
8
  supportsYolo: true,
7
9
  suppressStdErr: true,
8
10
  npmPackage: "@github/copilot",
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const cursorAgent = {
3
+ label: "Cursor CLI",
3
4
  command: "cursor",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["-p"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const deepAgentsAgent = {
3
+ label: "Deep Agents CLI",
3
4
  command: "deepagents",
4
- promptCommandLineArgs: ["--non-interactive"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["--non-interactive"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const droidAgent = {
3
+ label: "Droid CLI",
3
4
  command: "droid",
4
- promptCommandLineArgs: ["exec"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["exec"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  npmPackage: "@factory/cli",
8
9
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
@@ -16,9 +16,10 @@ export function renderPolicyToml(allowedTools) {
16
16
  ].join("\n");
17
17
  }
18
18
  export const geminiAgent = {
19
+ label: "Gemini CLI",
19
20
  command: "gemini",
20
- promptCommandLineArgs: ["--prompt"],
21
- versionCommandLineArg: "--version",
21
+ promptArgs: ["--prompt"],
22
+ probeArg: "--version",
22
23
  supportsPermissions: true,
23
24
  supportsYolo: true,
24
25
  npmPackage: "@google/gemini-cli",
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const gooseAgent = {
3
+ label: "Goose CLI",
3
4
  command: "goose",
4
- promptCommandLineArgs: ["run", "--text"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["run", "--text"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const hermesAgent = {
3
+ label: "Hermes Agent",
3
4
  command: "hermes",
4
- promptCommandLineArgs: ["chat", "-q"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["chat", "-q"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const kimiAgent = {
3
+ label: "Kimi Code",
3
4
  command: "kimi",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["-p"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const kiroAgent = {
3
+ label: "Kiro CLI",
3
4
  command: "kiro-cli",
4
- promptCommandLineArgs: ["--no-interactive"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["--no-interactive"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const yolo = extraPermissions === "yolo";
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const openClawAgent = {
3
+ label: "OpenClaw",
3
4
  command: "openclaw",
4
- promptCommandLineArgs: ["agent", "--local", "--agent", "main", "--message"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["agent", "--local", "--agent", "main", "--message"],
6
+ probeArg: "--version",
6
7
  npmPackage: "openclaw",
7
8
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
8
9
  const prompt = followupPrompt ?? getAgentInstructions(task);
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const openCodeAgent = {
3
+ label: "OpenCode",
3
4
  command: "opencode",
4
- promptCommandLineArgs: ["run"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["run"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  npmPackage: "opencode-ai",
8
9
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
@@ -1,8 +1,9 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const qoderAgent = {
3
+ label: "Qoder CLI",
3
4
  command: "qodercli",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["-p"],
6
+ probeArg: "--version",
6
7
  supportsYolo: true,
7
8
  npmPackage: "@qoder-ai/qodercli",
8
9
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
@@ -1,8 +1,10 @@
1
1
  import { getAgentInstructions } from "./shared-prompt.js";
2
2
  export const qwenAgent = {
3
+ label: "Qwen Code",
3
4
  command: "qwen",
4
- promptCommandLineArgs: ["-p"],
5
- versionCommandLineArg: "--version",
5
+ promptArgs: ["-p"],
6
+ probeArg: "--version",
7
+ authArgs: ["auth", "qwen-oauth"],
6
8
  supportsYolo: true,
7
9
  npmPackage: "@qwen-code/qwen-code",
8
10
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
@@ -1,7 +1,7 @@
1
1
  import * as readline from "readline";
2
2
  import { spawnSync } from "child_process";
3
3
  import { loadConfig, saveConfig } from "../config.js";
4
- import { detectAgents, getNpmInstalledVersion, listInstallableAgents } from "../agents/agent.js";
4
+ import { detectAgents, getAgent, getNpmInstalledVersion, listInstallableAgents } from "../agents/agent.js";
5
5
  import { getPlatform } from "../platform/index.js";
6
6
  import { pairCommand } from "./pair.js";
7
7
  import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
@@ -17,18 +17,25 @@ export async function initCommand() {
17
17
  console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
18
18
  console.log(`and ${cyan("Privacy Policy")} (https://www.palmier.me/privacy).\n`);
19
19
  console.log("Detecting installed agents...");
20
- let agents = await detectAgents();
21
- if (agents.length > 0) {
22
- console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}`);
20
+ let previousConfig = null;
21
+ try {
22
+ previousConfig = loadConfig();
23
23
  }
24
- agents = await offerAgentInstall(agents);
24
+ catch { /* first init */ }
25
+ 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
+ });
25
33
  if (agents.length === 0) {
26
34
  console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one supported agent CLI.\n`);
27
35
  console.log(`See supported agents: https://www.palmier.me/agents\n`);
28
36
  console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
29
37
  process.exit(1);
30
38
  }
31
- console.log(`\n Agents: ${green(agents.map((a) => a.label).join(", "))}\n`);
32
39
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
33
40
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
34
41
  try {
@@ -56,7 +63,7 @@ export async function initCommand() {
56
63
  }
57
64
  console.log(` ${dim("Remote (web):")} ${cyan("https://app.palmier.me")}`);
58
65
  console.log(` Pair a browser on any device. Traffic always goes through the relay.\n`);
59
- console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
66
+ console.log(` ${dim("Agents:")} ${agents.map((a) => a.version ? `${a.label} v${a.version}` : a.label).join(", ")}\n`);
60
67
  const existingTasks = listTasks(process.cwd());
61
68
  if (existingTasks.length > 0) {
62
69
  console.log(` ${dim("Recover tasks:")} ${existingTasks.length} existing task(s) found:`);
@@ -71,11 +78,7 @@ export async function initCommand() {
71
78
  rl.close();
72
79
  return;
73
80
  }
74
- let existingHostId;
75
- try {
76
- existingHostId = loadConfig().hostId;
77
- }
78
- catch { /* first init */ }
81
+ const existingHostId = previousConfig?.hostId;
79
82
  const serverUrl = "https://app.palmier.me";
80
83
  let registerResponse;
81
84
  while (true) {
@@ -108,7 +111,13 @@ export async function initCommand() {
108
111
  };
109
112
  saveConfig(config);
110
113
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
111
- getPlatform().installDaemon(config);
114
+ const platform = getPlatform();
115
+ platform.installDaemon(config);
116
+ if (previousConfig) {
117
+ // Re-init: a daemon is already running with stale in-memory config.
118
+ // Restart so it picks up the new agents/versions for host.info.
119
+ await platform.restartDaemon();
120
+ }
112
121
  // Task recovery runs in the daemon (palmier serve) because that process
113
122
  // is elevated and can create S4U scheduled tasks.
114
123
  console.log("\nStarting pairing...");
@@ -120,49 +129,86 @@ export async function initCommand() {
120
129
  throw err;
121
130
  }
122
131
  }
123
- async function offerAgentInstall(currentAgents) {
124
- let agents = currentAgents;
132
+ async function offerAgentInstall(agents, onAgentInstalled) {
125
133
  while (true) {
126
134
  const detectedKeys = new Set(agents.map((a) => a.key));
127
135
  const missing = listInstallableAgents()
128
136
  .filter((a) => !detectedKeys.has(a.key))
129
137
  .sort((a, b) => a.label.localeCompare(b.label));
130
138
  if (missing.length === 0)
131
- return agents;
132
- const canFinish = agents.length > 0;
133
- const message = canFinish
139
+ return;
140
+ const hasAgents = agents.length > 0;
141
+ const message = hasAgents
134
142
  ? `\n${bold("Install additional agents?")} The following supported agents can be installed:`
135
143
  : `\n${red("No agent CLIs detected.")} Palmier can install one for you via npm:`;
136
144
  const installChoices = missing.map((a) => ({
137
145
  label: a.freeUsage ? `${a.label} ${green(`[${a.freeUsage}]`)}` : a.label,
138
- hint: `npm install -g ${a.npmPackage}`,
146
+ hint: `${a.npmPackage}`,
139
147
  }));
140
- const choices = canFinish
141
- ? [{ label: "No — continue setup", hint: "skip installation" }, ...installChoices]
148
+ const choices = hasAgents
149
+ ? [{ label: "No — continue to the next step ", hint: "skip installation" }, ...installChoices]
142
150
  : installChoices;
143
151
  const idx = await selectFromList(message, choices);
144
152
  if (idx === null)
145
- return agents;
146
- if (canFinish && idx === 0)
147
- return agents;
148
- const choice = missing[canFinish ? idx - 1 : idx];
153
+ return;
154
+ if (hasAgents && idx === 0)
155
+ return;
156
+ const choice = missing[hasAgents ? idx - 1 : idx];
149
157
  if (!installAgentPackage(choice))
150
- return agents;
151
- console.log(`\nRedetecting agents...`);
152
- agents = await detectAgents(agents);
153
- const installedAgent = agents.find((a) => a.key === choice.key);
154
- if (!installedAgent) {
155
- console.log(`${red(`${choice.label} still not detected after install.`)} It may not be on PATH yet — open a new terminal and run ${cyan("palmier init")} again.`);
156
- return agents;
157
- }
158
- const version = getNpmInstalledVersion(choice.npmPackage);
159
- if (version)
160
- installedAgent.version = version;
158
+ return;
161
159
  console.log(green(` ${choice.label} installed.`));
162
- console.log(`\n${bold("Next: authenticate the CLI.")}`);
163
- console.log(` Run ${cyan(choice.command)} in another terminal and follow the sign-in prompts.`);
164
- console.log(` Palmier will use the CLI on your behalf once it's signed in.`);
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;
165
210
  }
211
+ console.log(green(`Successfully authenticated ${label}.\n`));
166
212
  }
167
213
  function installAgentPackage(agent) {
168
214
  console.log(`\nInstalling ${cyan(agent.npmPackage)}...\n`);