palmier 0.2.0 → 0.2.2
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/CLAUDE.md +5 -1
- package/README.md +135 -45
- package/dist/agents/agent.d.ts +26 -0
- package/dist/agents/agent.js +32 -0
- package/dist/agents/claude.d.ts +8 -0
- package/dist/agents/claude.js +35 -0
- package/dist/agents/codex.d.ts +8 -0
- package/dist/agents/codex.js +41 -0
- package/dist/agents/gemini.d.ts +8 -0
- package/dist/agents/gemini.js +39 -0
- package/dist/agents/openclaw.d.ts +8 -0
- package/dist/agents/openclaw.js +25 -0
- package/dist/agents/shared-prompt.d.ts +11 -0
- package/dist/agents/shared-prompt.js +26 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +19 -0
- package/dist/commands/info.d.ts +5 -0
- package/dist/commands/info.js +40 -0
- package/dist/commands/init.d.ts +7 -2
- package/dist/commands/init.js +139 -49
- package/dist/commands/mcpserver.d.ts +2 -0
- package/dist/commands/mcpserver.js +75 -0
- package/dist/commands/pair.d.ts +6 -0
- package/dist/commands/pair.js +140 -0
- package/dist/commands/plan-generation.md +32 -0
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +258 -114
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +16 -228
- package/dist/commands/sessions.d.ts +4 -0
- package/dist/commands/sessions.js +30 -0
- package/dist/commands/task-generation.md +1 -1
- package/dist/config.d.ts +5 -5
- package/dist/config.js +24 -6
- package/dist/index.js +58 -5
- package/dist/nats-client.d.ts +3 -3
- package/dist/nats-client.js +2 -2
- package/dist/rpc-handler.d.ts +6 -0
- package/dist/rpc-handler.js +367 -0
- package/dist/session-store.d.ts +12 -0
- package/dist/session-store.js +57 -0
- package/dist/spawn-command.d.ts +26 -0
- package/dist/spawn-command.js +48 -0
- package/dist/systemd.d.ts +2 -2
- package/dist/task.d.ts +45 -2
- package/dist/task.js +155 -14
- package/dist/transports/http-transport.d.ts +6 -0
- package/dist/transports/http-transport.js +243 -0
- package/dist/transports/nats-transport.d.ts +6 -0
- package/dist/transports/nats-transport.js +69 -0
- package/dist/types.d.ts +30 -13
- package/package.json +4 -3
- package/src/agents/agent.ts +62 -0
- package/src/agents/claude.ts +39 -0
- package/src/agents/codex.ts +46 -0
- package/src/agents/gemini.ts +43 -0
- package/src/agents/openclaw.ts +29 -0
- package/src/agents/shared-prompt.ts +26 -0
- package/src/commands/agents.ts +20 -0
- package/src/commands/info.ts +44 -0
- package/src/commands/init.ts +229 -121
- package/src/commands/mcpserver.ts +92 -0
- package/src/commands/pair.ts +163 -0
- package/src/commands/plan-generation.md +32 -0
- package/src/commands/run.ts +323 -129
- package/src/commands/serve.ts +26 -287
- package/src/commands/sessions.ts +32 -0
- package/src/config.ts +30 -10
- package/src/index.ts +67 -6
- package/src/nats-client.ts +4 -4
- package/src/rpc-handler.ts +421 -0
- package/src/session-store.ts +68 -0
- package/src/spawn-command.ts +78 -0
- package/src/systemd.ts +2 -2
- package/src/task.ts +166 -16
- package/src/transports/http-transport.ts +290 -0
- package/src/transports/nats-transport.ts +82 -0
- package/src/types.ts +36 -13
- package/src/commands/task-generation.md +0 -28
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
|
+
|
|
6
|
+
export class ClaudeAgent implements AgentTool {
|
|
7
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
|
+
return {
|
|
9
|
+
command: "claude",
|
|
10
|
+
args: ["-p", prompt],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
+
prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
|
|
16
|
+
const args = ["-c", "--permission-mode", "acceptEdits", "-p", prompt];
|
|
17
|
+
|
|
18
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
19
|
+
for (const p of allPerms) {
|
|
20
|
+
args.push("--allowedTools", p.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { command: "claude", args };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async init(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
execSync("claude --version");
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.warn("Warning: failed to install MCP for Claude:", err instanceof Error ? err.message : err);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
|
+
|
|
6
|
+
export class CodexAgent implements AgentTool {
|
|
7
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
|
+
// TODO: fill in
|
|
9
|
+
return {
|
|
10
|
+
command: "codex",
|
|
11
|
+
args: ["exec", "--skip-git-repo-check",prompt],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
|
|
17
|
+
// TODO: Update sandbox to workspace-write once https://github.com/openai/codex/issues/12572
|
|
18
|
+
// is fixed.
|
|
19
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
20
|
+
|
|
21
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
22
|
+
for (const p of allPerms) {
|
|
23
|
+
args.push("--config");
|
|
24
|
+
args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
args.push(prompt);
|
|
28
|
+
args.push("resume", "--last");
|
|
29
|
+
|
|
30
|
+
return { command: "codex", args };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init(): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
execSync("codex --version");
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
execSync("codex mcp add palmier palmier mcpserver");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.warn("Warning: failed to install MCP for Codex:", err instanceof Error ? err.message : err);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
|
+
|
|
6
|
+
export class GeminiAgent implements AgentTool {
|
|
7
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
|
+
// TODO: fill in
|
|
9
|
+
return {
|
|
10
|
+
command: "gemini",
|
|
11
|
+
args: ["--approval-mode", "auto_edit","--prompt", prompt],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
prompt = prompt ?? (task.body || task.frontmatter.user_prompt);
|
|
17
|
+
const args = ["--resume", "--prompt", prompt + TASK_OUTCOME_SUFFIX];
|
|
18
|
+
|
|
19
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
|
+
if (allPerms.length > 0) {
|
|
21
|
+
args.push("--allowed-tools");
|
|
22
|
+
for (const p of allPerms) {
|
|
23
|
+
args.push(p.name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { command: "gemini", args };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
execSync("gemini --version");
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
execSync("gemini mcp add --scope user palmier palmier mcpserver");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn("Warning: failed to install MCP for Gemini:", err instanceof Error ? err.message : err);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
|
+
|
|
6
|
+
export class ClaudeAgent implements AgentTool {
|
|
7
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
|
+
return {
|
|
9
|
+
command: "openclaw",
|
|
10
|
+
args: ["agent", "--message", prompt],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
+
prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
|
|
16
|
+
const args = ["agent", "--message", prompt];
|
|
17
|
+
|
|
18
|
+
return { command: "openclaw", args };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init(): Promise<boolean> {
|
|
22
|
+
try {
|
|
23
|
+
execSync("openclaw --version");
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appended to the end of every task prompt.
|
|
3
|
+
* Instructs the agent to output a clear success/failure marker
|
|
4
|
+
* so that palmier can determine the task outcome.
|
|
5
|
+
*/
|
|
6
|
+
export const TASK_OUTCOME_SUFFIX = `
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
|
|
10
|
+
[PALMIER_REPORT] report.md
|
|
11
|
+
[PALMIER_REPORT] summary.md
|
|
12
|
+
|
|
13
|
+
When you are done, output exactly one of these markers as the very last line:
|
|
14
|
+
- Success: [PALMIER_TASK_SUCCESS]
|
|
15
|
+
- Failure: [PALMIER_TASK_FAILURE]
|
|
16
|
+
Do not wrap them in code blocks or add text on the same line.
|
|
17
|
+
|
|
18
|
+
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.
|
|
19
|
+
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
20
|
+
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
21
|
+
[PALMIER_PERMISSION] Write | Write generated output files`;
|
|
22
|
+
|
|
23
|
+
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
24
|
+
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
25
|
+
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
26
|
+
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
2
|
+
import { detectAgents } from "../agents/agent.js";
|
|
3
|
+
|
|
4
|
+
export async function agentsCommand(): Promise<void> {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
|
|
7
|
+
console.log("Detecting installed agents...");
|
|
8
|
+
const agents = await detectAgents();
|
|
9
|
+
config.agents = agents;
|
|
10
|
+
saveConfig(config);
|
|
11
|
+
|
|
12
|
+
if (agents.length === 0) {
|
|
13
|
+
console.log("No agent CLIs detected.");
|
|
14
|
+
} else {
|
|
15
|
+
console.log("Detected agents:");
|
|
16
|
+
for (const a of agents) {
|
|
17
|
+
console.log(` ${a.key} — ${a.label}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect the first non-internal IPv4 address.
|
|
6
|
+
*/
|
|
7
|
+
function detectLanIp(): string {
|
|
8
|
+
const interfaces = os.networkInterfaces();
|
|
9
|
+
for (const name of Object.keys(interfaces)) {
|
|
10
|
+
for (const iface of interfaces[name] ?? []) {
|
|
11
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
12
|
+
return iface.address;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return "127.0.0.1";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Print host connection info for setting up clients.
|
|
21
|
+
*/
|
|
22
|
+
export async function infoCommand(): Promise<void> {
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
const mode = config.mode ?? "nats";
|
|
25
|
+
|
|
26
|
+
console.log(`Mode: ${mode}`);
|
|
27
|
+
console.log(`Host ID: ${config.hostId}`);
|
|
28
|
+
|
|
29
|
+
if (mode === "lan" || mode === "auto") {
|
|
30
|
+
const lanIp = detectLanIp();
|
|
31
|
+
const port = config.directPort ?? 7400;
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log("Direct connection info:");
|
|
34
|
+
console.log(` Address: ${lanIp}:${port}`);
|
|
35
|
+
console.log(` Token: ${config.directToken}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (mode === "nats" || mode === "auto") {
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("NATS connection info:");
|
|
41
|
+
console.log(` URL: ${config.natsUrl}`);
|
|
42
|
+
console.log(` WS: ${config.natsWsUrl}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,121 +1,229 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import { randomUUID, randomBytes } from "crypto";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { saveConfig } from "../config.js";
|
|
9
|
+
import { detectAgents } from "../agents/agent.js";
|
|
10
|
+
import { pairCommand } from "./pair.js";
|
|
11
|
+
import type { HostConfig } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export interface InitOptions {
|
|
14
|
+
server?: string;
|
|
15
|
+
lan?: boolean;
|
|
16
|
+
host?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provision this host. Two flows:
|
|
22
|
+
* - palmier init --lan → standalone LAN mode, no server needed
|
|
23
|
+
* - palmier init --server <url> → register with server, prompts for nats/auto mode
|
|
24
|
+
*/
|
|
25
|
+
export async function initCommand(options: InitOptions): Promise<void> {
|
|
26
|
+
if (options.lan) {
|
|
27
|
+
await initLan(options);
|
|
28
|
+
} else if (options.server) {
|
|
29
|
+
await initWithServer(options);
|
|
30
|
+
} else {
|
|
31
|
+
console.error("Either --lan or --server <url> is required.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Flow A: Standalone LAN mode. No web server needed.
|
|
38
|
+
*/
|
|
39
|
+
async function initLan(options: InitOptions): Promise<void> {
|
|
40
|
+
const hostId = randomUUID();
|
|
41
|
+
const directToken = randomBytes(32).toString("hex");
|
|
42
|
+
const directPort = options.port ?? 7400;
|
|
43
|
+
const lanIp = options.host ?? detectLanIp();
|
|
44
|
+
|
|
45
|
+
const config: HostConfig = {
|
|
46
|
+
hostId,
|
|
47
|
+
mode: "lan",
|
|
48
|
+
directPort,
|
|
49
|
+
directToken,
|
|
50
|
+
projectRoot: process.cwd(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
console.log("Detecting installed agents...");
|
|
54
|
+
config.agents = await detectAgents();
|
|
55
|
+
saveConfig(config);
|
|
56
|
+
|
|
57
|
+
console.log(`Host provisioned (LAN mode). ID: ${hostId}`);
|
|
58
|
+
console.log("Config saved to ~/.config/palmier/host.json");
|
|
59
|
+
|
|
60
|
+
installSystemdService(config);
|
|
61
|
+
|
|
62
|
+
// Auto-enter pair mode so user can connect their PWA immediately
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Starting pairing...");
|
|
65
|
+
await pairCommand();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Flow B: Register directly with server. No provisioning token needed.
|
|
70
|
+
*/
|
|
71
|
+
async function initWithServer(options: InitOptions): Promise<void> {
|
|
72
|
+
const serverUrl = options.server!;
|
|
73
|
+
|
|
74
|
+
console.log(`Registering host at ${serverUrl}...`);
|
|
75
|
+
|
|
76
|
+
let registerResponse: {
|
|
77
|
+
hostId: string;
|
|
78
|
+
natsUrl: string;
|
|
79
|
+
natsWsUrl: string;
|
|
80
|
+
natsToken: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`${serverUrl}/api/hosts/register`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const body = await res.text();
|
|
92
|
+
console.error(`Failed to register host: ${res.status} ${res.statusText}\n${body}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
registerResponse = (await res.json()) as typeof registerResponse;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Failed to reach server: ${err}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Prompt for connection mode
|
|
103
|
+
const mode = await promptMode();
|
|
104
|
+
|
|
105
|
+
// Build config
|
|
106
|
+
const config: HostConfig = {
|
|
107
|
+
hostId: registerResponse.hostId,
|
|
108
|
+
projectRoot: process.cwd(),
|
|
109
|
+
mode,
|
|
110
|
+
natsUrl: registerResponse.natsUrl,
|
|
111
|
+
natsWsUrl: registerResponse.natsWsUrl,
|
|
112
|
+
natsToken: registerResponse.natsToken,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (mode === "auto") {
|
|
116
|
+
const directToken = randomBytes(32).toString("hex");
|
|
117
|
+
const directPort = options.port ?? 7400;
|
|
118
|
+
const lanIp = options.host ?? detectLanIp();
|
|
119
|
+
|
|
120
|
+
config.directPort = directPort;
|
|
121
|
+
config.directToken = directToken;
|
|
122
|
+
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log("Direct connection info (for LAN clients):");
|
|
125
|
+
console.log(` Address: ${lanIp}:${directPort}`);
|
|
126
|
+
console.log(` Token: ${directToken}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log("Detecting installed agents...");
|
|
130
|
+
config.agents = await detectAgents();
|
|
131
|
+
saveConfig(config);
|
|
132
|
+
console.log(`Host provisioned (${mode} mode). ID: ${config.hostId}`);
|
|
133
|
+
console.log("Config saved to ~/.config/palmier/host.json");
|
|
134
|
+
|
|
135
|
+
installSystemdService(config);
|
|
136
|
+
|
|
137
|
+
// Auto-enter pair mode so user can connect their PWA immediately
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log("Starting pairing...");
|
|
140
|
+
await pairCommand();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Prompt user to select connection mode.
|
|
145
|
+
*/
|
|
146
|
+
async function promptMode(): Promise<"nats" | "auto"> {
|
|
147
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
const question = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
|
|
149
|
+
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log("Connection mode:");
|
|
152
|
+
console.log(" 1) auto — Both LAN and NATS (recommended)");
|
|
153
|
+
console.log(" 2) nats — NATS only");
|
|
154
|
+
console.log("");
|
|
155
|
+
|
|
156
|
+
const answer = await question("Select [1]: ");
|
|
157
|
+
rl.close();
|
|
158
|
+
|
|
159
|
+
if (answer.trim() === "2" || answer.trim().toLowerCase() === "nats") {
|
|
160
|
+
return "nats";
|
|
161
|
+
}
|
|
162
|
+
return "auto";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Detect the first non-internal IPv4 address.
|
|
167
|
+
*/
|
|
168
|
+
function detectLanIp(): string {
|
|
169
|
+
const interfaces = os.networkInterfaces();
|
|
170
|
+
for (const name of Object.keys(interfaces)) {
|
|
171
|
+
for (const iface of interfaces[name] ?? []) {
|
|
172
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
173
|
+
return iface.address;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return "127.0.0.1";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Install systemd user service for palmier serve.
|
|
182
|
+
*/
|
|
183
|
+
function installSystemdService(config: HostConfig): void {
|
|
184
|
+
const unitDir = path.join(homedir(), ".config", "systemd", "user");
|
|
185
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
188
|
+
|
|
189
|
+
const serviceContent = `[Unit]
|
|
190
|
+
Description=Palmier Host
|
|
191
|
+
After=network-online.target
|
|
192
|
+
Wants=network-online.target
|
|
193
|
+
|
|
194
|
+
[Service]
|
|
195
|
+
Type=simple
|
|
196
|
+
ExecStart=${palmierBin} serve
|
|
197
|
+
WorkingDirectory=${config.projectRoot}
|
|
198
|
+
Restart=on-failure
|
|
199
|
+
RestartSec=5
|
|
200
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
201
|
+
|
|
202
|
+
[Install]
|
|
203
|
+
WantedBy=default.target
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const servicePath = path.join(unitDir, "palmier.service");
|
|
207
|
+
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
|
208
|
+
console.log("Systemd service installed at:", servicePath);
|
|
209
|
+
|
|
210
|
+
// Enable and start the service
|
|
211
|
+
try {
|
|
212
|
+
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
213
|
+
execSync("systemctl --user enable --now palmier.service", { stdio: "inherit" });
|
|
214
|
+
console.log("Palmier host service enabled and started.");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`Warning: failed to enable systemd service: ${err}`);
|
|
217
|
+
console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Enable lingering so service runs without active login session
|
|
221
|
+
try {
|
|
222
|
+
execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
223
|
+
console.log("Login lingering enabled.");
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error(`Warning: failed to enable linger: ${err}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log("\nHost initialization complete!");
|
|
229
|
+
}
|