@foxden-app/foxclaw 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/.env.example CHANGED
@@ -29,7 +29,7 @@ TELEGRAM_PREVIEW_THROTTLE_MS=800
29
29
  THREAD_LIST_LIMIT=10
30
30
  CODEX_CLI_BIN=/absolute/path/to/codex
31
31
 
32
- # Weixin (iLink): run `npm run weixin-login` once, then enable:
32
+ # Weixin (iLink): run `foxclaw weixin-login` once, then enable:
33
33
  # WX_ENABLED=true
34
34
  # Comma-separated iLink user ids allowed to chat (see account JSON linkedIlinkUserId)
35
35
  # WX_ALLOWED_ILINK_USER_IDS=
package/README.md CHANGED
@@ -31,17 +31,24 @@ FoxClaw 是 Foxden agents 的本地执行爪。
31
31
  - 来自 `@BotFather` 的 Telegram bot token
32
32
  - 你的 Telegram 数字用户 ID
33
33
 
34
- ## 快速设置
34
+ ## npm 快速设置
35
35
 
36
36
  ```bash
37
- git clone https://github.com/foxden-app/foxclaw.git
38
- cd foxclaw
39
- npm install
40
- cp .env.example .env
41
- $EDITOR .env
42
- npm run build
43
- npm run doctor
44
- npm run serve
37
+ npm install -g @foxden-app/foxclaw
38
+ foxclaw init
39
+ $EDITOR ~/.foxclaw/.env
40
+ foxclaw doctor
41
+ foxclaw serve
42
+ ```
43
+
44
+ pnpm 用户可以使用:
45
+
46
+ ```bash
47
+ pnpm add -g @foxden-app/foxclaw
48
+ foxclaw init
49
+ $EDITOR ~/.foxclaw/.env
50
+ foxclaw doctor
51
+ foxclaw serve
45
52
  ```
46
53
 
47
54
  运行 `doctor` 或 `serve` 前先编辑 `.env`。私聊模式的最小配置:
@@ -54,6 +61,8 @@ DEFAULT_APPROVAL_POLICY=on-request
54
61
  DEFAULT_SANDBOX_MODE=workspace-write
55
62
  ```
56
63
 
64
+ 默认配置文件是 `~/.foxclaw/.env`。如果你想把配置放在别处,可以设置 `FOXCLAW_ENV=/path/to/.env`。
65
+
57
66
  FoxClaw 只接受来自 `TG_ALLOWED_USER_ID` 的消息。把机器人放进群组并不会让每个群成员都能使用它。
58
67
 
59
68
  <details>
@@ -79,7 +88,7 @@ FoxClaw 只接受来自 `TG_ALLOWED_USER_ID` 的消息。把机器人放进群
79
88
  Linux 用户级 systemd:
80
89
 
81
90
  ```bash
82
- npm run install-systemd
91
+ foxclaw install-systemd
83
92
  systemctl --user status foxclaw.service
84
93
  journalctl --user -u foxclaw.service -f
85
94
  ```
@@ -87,7 +96,7 @@ journalctl --user -u foxclaw.service -f
87
96
  macOS launchd:
88
97
 
89
98
  ```bash
90
- ./scripts/launchd/install.sh
99
+ foxclaw install-launchd
91
100
  ```
92
101
 
93
102
  默认运行时文件存储在 `~/.foxclaw`:
@@ -109,14 +118,14 @@ FoxClaw 最初 fork 自 `Gan-Xing/telegram-codex-app-bridge`,并继续以 MIT
109
118
  ```bash
110
119
  systemctl --user disable --now telegram-codex-app-bridge.service 2>/dev/null || true
111
120
  test -e ~/.foxclaw || cp -a ~/.telegram-codex-app-bridge ~/.foxclaw
112
- npm run install-systemd
121
+ foxclaw install-systemd
113
122
  ```
114
123
 
115
124
  如果使用 launchd 安装,先卸载旧 plist(如存在):
116
125
 
117
126
  ```bash
118
127
  launchctl unload ~/Library/LaunchAgents/com.ganxing.telegram-codex-app-bridge.plist 2>/dev/null || true
119
- ./scripts/launchd/install.sh
128
+ foxclaw install-launchd
120
129
  ```
121
130
 
122
131
  旧运行时目录不会被自动读取。如果你想保留现有绑定、缓存线程列表、审批和状态数据,请手动复制一次。
@@ -125,7 +134,7 @@ launchctl unload ~/Library/LaunchAgents/com.ganxing.telegram-codex-app-bridge.pl
125
134
 
126
135
  1. 使用 `@BotFather` 创建机器人,并把 token 填入 `TG_BOT_TOKEN`。
127
136
  2. 获取你的 Telegram 数字用户 ID,并填入 `TG_ALLOWED_USER_ID`。
128
- 3. 使用 `npm run serve` 或服务安装器启动 FoxClaw。
137
+ 3. 使用 `foxclaw serve` 或服务安装器启动 FoxClaw。
129
138
  4. 打开与机器人的私聊并发送 `/help`。
130
139
 
131
140
  可选的群组/话题配置:
@@ -216,7 +225,7 @@ WX_ALLOWED_ILINK_USER_IDS=
216
225
  构建后运行一次二维码登录助手:
217
226
 
218
227
  ```bash
219
- npm run weixin-login
228
+ foxclaw weixin-login
220
229
  ```
221
230
 
222
231
  微信运行时文件默认存储在 `~/.foxclaw/weixin`。
@@ -232,11 +241,10 @@ npm run weixin-login
232
241
  ## 运维命令
233
242
 
234
243
  ```bash
235
- npm run build
236
- npm run doctor
237
- npm run status
238
- npm run install-systemd
239
- npm run uninstall-systemd
244
+ foxclaw doctor
245
+ foxclaw status
246
+ foxclaw install-systemd
247
+ foxclaw uninstall-systemd
240
248
  ```
241
249
 
242
250
  ## 贡献
package/README_EN.md CHANGED
@@ -31,17 +31,24 @@ The minimum install needs only a Telegram bot token, your numeric Telegram user
31
31
  - A Telegram bot token from `@BotFather`
32
32
  - Your Telegram numeric user id
33
33
 
34
- ## Quick Setup
34
+ ## npm Quick Setup
35
35
 
36
36
  ```bash
37
- git clone https://github.com/foxden-app/foxclaw.git
38
- cd foxclaw
39
- npm install
40
- cp .env.example .env
41
- $EDITOR .env
42
- npm run build
43
- npm run doctor
44
- npm run serve
37
+ npm install -g @foxden-app/foxclaw
38
+ foxclaw init
39
+ $EDITOR ~/.foxclaw/.env
40
+ foxclaw doctor
41
+ foxclaw serve
42
+ ```
43
+
44
+ pnpm users can use:
45
+
46
+ ```bash
47
+ pnpm add -g @foxden-app/foxclaw
48
+ foxclaw init
49
+ $EDITOR ~/.foxclaw/.env
50
+ foxclaw doctor
51
+ foxclaw serve
45
52
  ```
46
53
 
47
54
  Edit `.env` before running `doctor` or `serve`. Minimum private-chat config:
@@ -54,6 +61,8 @@ DEFAULT_APPROVAL_POLICY=on-request
54
61
  DEFAULT_SANDBOX_MODE=workspace-write
55
62
  ```
56
63
 
64
+ The default config file is `~/.foxclaw/.env`. Set `FOXCLAW_ENV=/path/to/.env` if you want to keep it somewhere else.
65
+
57
66
  FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a group does not make it available to every group member.
58
67
 
59
68
  <details>
@@ -79,7 +88,7 @@ FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a gr
79
88
  Linux user systemd:
80
89
 
81
90
  ```bash
82
- npm run install-systemd
91
+ foxclaw install-systemd
83
92
  systemctl --user status foxclaw.service
84
93
  journalctl --user -u foxclaw.service -f
85
94
  ```
@@ -87,7 +96,7 @@ journalctl --user -u foxclaw.service -f
87
96
  macOS launchd:
88
97
 
89
98
  ```bash
90
- ./scripts/launchd/install.sh
99
+ foxclaw install-launchd
91
100
  ```
92
101
 
93
102
  Default runtime files are stored under `~/.foxclaw`:
@@ -109,14 +118,14 @@ When upgrading an existing local install:
109
118
  ```bash
110
119
  systemctl --user disable --now telegram-codex-app-bridge.service 2>/dev/null || true
111
120
  test -e ~/.foxclaw || cp -a ~/.telegram-codex-app-bridge ~/.foxclaw
112
- npm run install-systemd
121
+ foxclaw install-systemd
113
122
  ```
114
123
 
115
124
  For launchd installs, unload the old plist if present:
116
125
 
117
126
  ```bash
118
127
  launchctl unload ~/Library/LaunchAgents/com.ganxing.telegram-codex-app-bridge.plist 2>/dev/null || true
119
- ./scripts/launchd/install.sh
128
+ foxclaw install-launchd
120
129
  ```
121
130
 
122
131
  The old runtime directory is not read automatically. Copy it once if you want to keep existing bindings, cached thread lists, approvals, and status data.
@@ -125,7 +134,7 @@ The old runtime directory is not read automatically. Copy it once if you want to
125
134
 
126
135
  1. Create a bot with `@BotFather` and copy the token into `TG_BOT_TOKEN`.
127
136
  2. Get your Telegram numeric user id and place it into `TG_ALLOWED_USER_ID`.
128
- 3. Start FoxClaw with `npm run serve` or the service installer.
137
+ 3. Start FoxClaw with `foxclaw serve` or the service installer.
129
138
  4. Open a private chat with the bot and send `/help`.
130
139
 
131
140
  Optional group/topic config:
@@ -216,7 +225,7 @@ WX_ALLOWED_ILINK_USER_IDS=
216
225
  Run the QR login helper once after building:
217
226
 
218
227
  ```bash
219
- npm run weixin-login
228
+ foxclaw weixin-login
220
229
  ```
221
230
 
222
231
  Weixin runtime files default to `~/.foxclaw/weixin`.
@@ -232,11 +241,10 @@ See [Troubleshooting](./docs/troubleshooting.md) for `doctor` failures, Telegram
232
241
  ## Operations
233
242
 
234
243
  ```bash
235
- npm run build
236
- npm run doctor
237
- npm run status
238
- npm run install-systemd
239
- npm run uninstall-systemd
244
+ foxclaw doctor
245
+ foxclaw status
246
+ foxclaw install-systemd
247
+ foxclaw uninstall-systemd
240
248
  ```
241
249
 
242
250
  ## Contributing
package/dist/config.d.ts CHANGED
@@ -7,6 +7,8 @@ export declare const DEFAULT_LOG_PATH: string;
7
7
  export declare const DEFAULT_LOCK_PATH: string;
8
8
  export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
9
9
  export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
10
+ export declare const DEFAULT_ENV_PATH: string;
11
+ export declare function loadEnv(): void;
10
12
  export interface AppConfig {
11
13
  tgBotToken: string;
12
14
  tgAllowedUserId: string;
package/dist/config.js CHANGED
@@ -3,15 +3,33 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import dotenv from 'dotenv';
6
- export const APP_HOME = path.join(os.homedir(), '.foxclaw');
6
+ export const APP_HOME = path.join(process.env.HOME || os.homedir(), '.foxclaw');
7
7
  export const DEFAULT_STORE_PATH = path.join(APP_HOME, 'data', 'bridge.sqlite');
8
8
  export const DEFAULT_STATUS_PATH = path.join(APP_HOME, 'runtime', 'status.json');
9
9
  export const DEFAULT_LOG_PATH = path.join(APP_HOME, 'logs', 'service.log');
10
10
  export const DEFAULT_LOCK_PATH = path.join(APP_HOME, 'runtime', 'bridge.lock');
11
11
  export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime', 'codex-app-server.json');
12
12
  export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
13
+ export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
14
+ let envLoaded = false;
15
+ export function loadEnv() {
16
+ if (envLoaded)
17
+ return;
18
+ envLoaded = true;
19
+ const explicitPath = process.env.FOXCLAW_ENV?.trim();
20
+ if (explicitPath) {
21
+ dotenv.config({ path: explicitPath });
22
+ return;
23
+ }
24
+ const cwdEnvPath = path.join(process.cwd(), '.env');
25
+ if (fs.existsSync(cwdEnvPath)) {
26
+ dotenv.config({ path: cwdEnvPath });
27
+ return;
28
+ }
29
+ dotenv.config({ path: DEFAULT_ENV_PATH });
30
+ }
13
31
  export function loadConfig() {
14
- dotenv.config();
32
+ loadEnv();
15
33
  const config = {
16
34
  tgBotToken: required('TG_BOT_TOKEN'),
17
35
  tgAllowedUserId: required('TG_ALLOWED_USER_ID'),
package/dist/main.js CHANGED
@@ -3,26 +3,33 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { spawnSync } from 'node:child_process';
6
- import dotenv from 'dotenv';
7
- import { APP_HOME, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig } from './config.js';
8
- import { BridgeMessagingRouter } from './channels/bridge_messaging_router.js';
9
- import { TelegramMessagingPort } from './channels/telegram/telegram_messaging_port.js';
10
- import { WeixinChannelAdapter } from './channels/weixin/weixin_channel_adapter.js';
11
- import { WeixinMessagingPort } from './channels/weixin/weixin_messaging_port.js';
12
- import { attachIlinkRuntimeFromBridgeLogger } from './channels/weixin/ilink/runtime_attach.js';
13
- import { startWeixinLoginWithQr, waitForWeixinLogin } from './channels/weixin/ilink/login_qr.js';
14
- import { accountFilePath, loadWeixinAccount, saveWeixinAccount } from './channels/weixin/account_store.js';
15
- import { Logger } from './logger.js';
16
- import { BridgeStore } from './store/database.js';
17
- import { TelegramGateway } from './telegram/gateway.js';
18
- import { CodexAppClient } from './codex_app/client.js';
19
- import { BridgeSessionCore } from './controller/controller.js';
20
- import { TelegramChannelAdapter } from './channels/telegram/telegram_channel_adapter.js';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig, loadEnv, } from './config.js';
21
8
  import { acquireProcessLock, LockHeldError } from './lock.js';
22
9
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
23
10
  const command = process.argv[2] || 'serve';
24
- dotenv.config();
11
+ loadEnv();
12
+ const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
13
+ const entryPoint = fileURLToPath(import.meta.url);
25
14
  async function main() {
15
+ if (command === 'init') {
16
+ initConfig();
17
+ return;
18
+ }
19
+ if (command === 'install-systemd') {
20
+ requireNode24(command);
21
+ installSystemd();
22
+ return;
23
+ }
24
+ if (command === 'uninstall-systemd') {
25
+ uninstallSystemd();
26
+ return;
27
+ }
28
+ if (command === 'install-launchd') {
29
+ requireNode24(command);
30
+ installLaunchd();
31
+ return;
32
+ }
26
33
  if (command === 'status') {
27
34
  const status = readRuntimeStatus(process.env.STATUS_PATH || DEFAULT_STATUS_PATH);
28
35
  if (!status) {
@@ -73,9 +80,28 @@ async function main() {
73
80
  process.exit(failed ? 1 : 0);
74
81
  }
75
82
  if (command === 'weixin-login') {
83
+ requireNode24(command);
76
84
  await runWeixinLoginCli();
77
85
  return;
78
86
  }
87
+ requireNode24(command);
88
+ await runServeCli();
89
+ }
90
+ async function runServeCli() {
91
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter },] = await Promise.all([
92
+ import('./channels/bridge_messaging_router.js'),
93
+ import('./channels/telegram/telegram_messaging_port.js'),
94
+ import('./channels/weixin/weixin_channel_adapter.js'),
95
+ import('./channels/weixin/weixin_messaging_port.js'),
96
+ import('./channels/weixin/ilink/runtime_attach.js'),
97
+ import('./channels/weixin/account_store.js'),
98
+ import('./logger.js'),
99
+ import('./store/database.js'),
100
+ import('./telegram/gateway.js'),
101
+ import('./codex_app/client.js'),
102
+ import('./controller/controller.js'),
103
+ import('./channels/telegram/telegram_channel_adapter.js'),
104
+ ]);
79
105
  const config = loadConfig();
80
106
  const logger = new Logger(config.logLevel, config.logPath);
81
107
  attachIlinkRuntimeFromBridgeLogger(logger, config.wxIlinkRouteTag);
@@ -139,7 +165,177 @@ async function main() {
139
165
  throw error;
140
166
  }
141
167
  }
168
+ function initConfig() {
169
+ const envPath = process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH;
170
+ fs.mkdirSync(path.dirname(envPath), { recursive: true });
171
+ if (fs.existsSync(envPath)) {
172
+ console.log(`Config already exists: ${envPath}`);
173
+ return;
174
+ }
175
+ const examplePath = path.join(packageRoot, '.env.example');
176
+ fs.copyFileSync(examplePath, envPath);
177
+ console.log(`Created ${envPath}`);
178
+ console.log('Edit it, then run: foxclaw doctor');
179
+ }
180
+ function installSystemd() {
181
+ if (!hasCommand('systemctl')) {
182
+ console.error('systemctl not found (need systemd)');
183
+ process.exit(1);
184
+ }
185
+ const unitName = 'foxclaw.service';
186
+ const userSystemdDir = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user');
187
+ const unitPath = path.join(userSystemdDir, unitName);
188
+ const configDir = path.dirname(process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH);
189
+ const nodeBin = process.execPath;
190
+ const nodeDir = path.dirname(nodeBin);
191
+ const pathValue = buildServicePath(nodeDir);
192
+ fs.mkdirSync(userSystemdDir, { recursive: true });
193
+ fs.mkdirSync(configDir, { recursive: true });
194
+ fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
195
+ const foxclawEnvLine = process.env.FOXCLAW_ENV?.trim()
196
+ ? `Environment=FOXCLAW_ENV=${systemdEscape(process.env.FOXCLAW_ENV.trim())}\n`
197
+ : '';
198
+ fs.writeFileSync(unitPath, `[Unit]
199
+ Description=FoxClaw local Codex execution bridge
200
+ Documentation=https://github.com/foxden-app/foxclaw
201
+ After=network-online.target
202
+ Wants=network-online.target
203
+ StartLimitIntervalSec=300
204
+ StartLimitBurst=5
205
+
206
+ [Service]
207
+ Type=simple
208
+ WorkingDirectory=${systemdEscape(configDir)}
209
+ Environment=HOME=${systemdEscape(process.env.HOME || '')}
210
+ Environment=USER=${systemdEscape(process.env.USER || '')}
211
+ Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
212
+ Environment=PATH=${systemdEscape(pathValue)}
213
+ ${foxclawEnvLine}ExecStart=${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve
214
+ Restart=always
215
+ RestartSec=10
216
+ TimeoutStopSec=45
217
+ KillMode=process
218
+
219
+ [Install]
220
+ WantedBy=default.target
221
+ `);
222
+ spawnChecked('systemctl', ['--user', 'daemon-reload']);
223
+ spawnChecked('systemctl', ['--user', 'enable', unitName]);
224
+ const restarted = spawnSync('systemctl', ['--user', 'restart', unitName], { stdio: 'inherit' });
225
+ if (restarted.status !== 0) {
226
+ spawnChecked('systemctl', ['--user', 'start', unitName]);
227
+ }
228
+ console.log(`Installed ${unitPath}`);
229
+ console.log(`Status: systemctl --user status ${unitName}`);
230
+ console.log(`Logs: journalctl --user -u ${unitName} -f`);
231
+ }
232
+ function uninstallSystemd() {
233
+ if (!hasCommand('systemctl')) {
234
+ console.error('systemctl not found');
235
+ process.exit(1);
236
+ }
237
+ const unitName = 'foxclaw.service';
238
+ const userSystemdDir = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user');
239
+ const unitPath = path.join(userSystemdDir, unitName);
240
+ spawnSync('systemctl', ['--user', 'disable', '--now', unitName], { stdio: 'inherit' });
241
+ fs.rmSync(unitPath, { force: true });
242
+ spawnChecked('systemctl', ['--user', 'daemon-reload']);
243
+ console.log(`Removed ${unitPath}`);
244
+ }
245
+ function installLaunchd() {
246
+ if (process.platform !== 'darwin') {
247
+ console.error('launchd install is only available on macOS');
248
+ process.exit(1);
249
+ }
250
+ const home = process.env.HOME || '';
251
+ const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
252
+ const configDir = path.dirname(process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH);
253
+ fs.mkdirSync(path.dirname(plist), { recursive: true });
254
+ fs.mkdirSync(configDir, { recursive: true });
255
+ fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
256
+ const foxclawEnvXml = process.env.FOXCLAW_ENV?.trim()
257
+ ? ` <key>FOXCLAW_ENV</key>
258
+ <string>${xmlEscape(process.env.FOXCLAW_ENV.trim())}</string>
259
+ `
260
+ : '';
261
+ fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
262
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
263
+ <plist version="1.0">
264
+ <dict>
265
+ <key>Label</key>
266
+ <string>app.foxden.foxclaw</string>
267
+ <key>ProgramArguments</key>
268
+ <array>
269
+ <string>${xmlEscape(process.execPath)}</string>
270
+ <string>${xmlEscape(entryPoint)}</string>
271
+ <string>serve</string>
272
+ </array>
273
+ <key>WorkingDirectory</key>
274
+ <string>${xmlEscape(configDir)}</string>
275
+ <key>EnvironmentVariables</key>
276
+ <dict>
277
+ <key>PATH</key>
278
+ <string>${xmlEscape(process.env.PATH || '')}</string>
279
+ <key>HOME</key>
280
+ <string>${xmlEscape(home)}</string>
281
+ <key>USER</key>
282
+ <string>${xmlEscape(process.env.USER || '')}</string>
283
+ <key>LOGNAME</key>
284
+ <string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
285
+ ${foxclawEnvXml}
286
+ </dict>
287
+ <key>RunAtLoad</key>
288
+ <true/>
289
+ <key>KeepAlive</key>
290
+ <true/>
291
+ <key>StandardOutPath</key>
292
+ <string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.out.log'))}</string>
293
+ <key>StandardErrorPath</key>
294
+ <string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.err.log'))}</string>
295
+ </dict>
296
+ </plist>
297
+ `);
298
+ spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
299
+ spawnChecked('launchctl', ['load', plist]);
300
+ console.log(`Installed ${plist}`);
301
+ }
302
+ function buildServicePath(nodeDir) {
303
+ const parts = [
304
+ path.join(process.env.HOME || '', '.local', 'bin'),
305
+ nodeDir,
306
+ '/usr/local/sbin',
307
+ '/usr/local/bin',
308
+ '/usr/sbin',
309
+ '/usr/bin',
310
+ '/sbin',
311
+ '/bin',
312
+ ];
313
+ return parts.filter((part, index) => part && parts.indexOf(part) === index).join(':');
314
+ }
315
+ function spawnChecked(commandName, args) {
316
+ const result = spawnSync(commandName, args, { stdio: 'inherit' });
317
+ if (result.status !== 0) {
318
+ process.exit(result.status || 1);
319
+ }
320
+ }
321
+ function systemdEscape(value) {
322
+ return value.replace(/\\/g, '\\\\').replace(/ /g, '\\x20');
323
+ }
324
+ function xmlEscape(value) {
325
+ return value
326
+ .replace(/&/g, '&amp;')
327
+ .replace(/</g, '&lt;')
328
+ .replace(/>/g, '&gt;')
329
+ .replace(/"/g, '&quot;')
330
+ .replace(/'/g, '&apos;');
331
+ }
142
332
  async function runWeixinLoginCli() {
333
+ const [{ attachIlinkRuntimeFromBridgeLogger }, { startWeixinLoginWithQr, waitForWeixinLogin }, { accountFilePath, saveWeixinAccount }, { Logger },] = await Promise.all([
334
+ import('./channels/weixin/ilink/runtime_attach.js'),
335
+ import('./channels/weixin/ilink/login_qr.js'),
336
+ import('./channels/weixin/account_store.js'),
337
+ import('./logger.js'),
338
+ ]);
143
339
  const logPath = process.env.LOG_PATH || DEFAULT_LOG_PATH;
144
340
  const level = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'warn' || process.env.LOG_LEVEL === 'error'
145
341
  ? process.env.LOG_LEVEL
@@ -228,6 +424,15 @@ function hasConfiguredCodexBin(binPath) {
228
424
  return false;
229
425
  }
230
426
  }
427
+ function requireNode24(commandName) {
428
+ if (Number(process.versions.node.split('.')[0]) >= 24)
429
+ return;
430
+ console.error(`FoxClaw ${commandName} requires Node.js 24+. Current Node.js is ${process.version}.`);
431
+ console.error('Install or activate Node 24, then reinstall/re-run FoxClaw:');
432
+ console.error(' nvm install 24 && nvm use 24');
433
+ console.error(' npm install -g @foxden-app/foxclaw@latest');
434
+ process.exit(1);
435
+ }
231
436
  function serializeError(error) {
232
437
  if (error instanceof Error) {
233
438
  return { message: error.message, stack: error.stack };
@@ -27,7 +27,8 @@ Send this to your agent on the target computer:
27
27
  Install FoxClaw on this machine.
28
28
 
29
29
  Repository:
30
- https://github.com/foxden-app/foxclaw.git
30
+ Published package:
31
+ @foxden-app/foxclaw
31
32
 
32
33
  Use private Telegram chat first. Do not configure group/topic mode unless I explicitly provide TG_ALLOWED_CHAT_ID or TG_ALLOWED_TOPIC_ID.
33
34
 
@@ -37,20 +38,20 @@ TG_ALLOWED_USER_ID=<paste numeric Telegram user id here>
37
38
  DEFAULT_CWD=<paste absolute working directory here>
38
39
 
39
40
  Tasks:
40
- 1. Inspect the machine first. Do not overwrite local changes if a FoxClaw or old telegram-codex-app-bridge repo already exists.
41
+ 1. Inspect the machine first. If a FoxClaw or old telegram-codex-app-bridge service already exists, report it before changing services.
41
42
  2. Ensure Node.js 24+ is available. If not, install or activate Node 24 with nvm.
42
43
  3. Ensure the Codex CLI exists and is logged in. If login is required, stop and tell me exactly what I need to do.
43
- 4. Clone or update https://github.com/foxden-app/foxclaw.git.
44
- 5. Write a local .env. Never commit .env.
45
- 6. Run npm install, npm run build, and npm run doctor.
46
- 7. Start FoxClaw in the foreground first and ask me to send /help and /status to the Telegram bot.
44
+ 4. Install or update FoxClaw with npm install -g @foxden-app/foxclaw@latest.
45
+ 5. Run foxclaw init, then write ~/.foxclaw/.env. Never print or commit the bot token.
46
+ 6. Run foxclaw doctor.
47
+ 7. Start FoxClaw in the foreground with foxclaw serve and ask me to send /help and /status to the Telegram bot.
47
48
  8. Only after the foreground test works, install the background service:
48
- - Linux: npm run install-systemd
49
- - macOS: ./scripts/launchd/install.sh
49
+ - Linux: foxclaw install-systemd
50
+ - macOS: foxclaw install-launchd
50
51
  9. Verify the final state:
51
52
  - foxclaw.service is active/enabled on Linux
52
53
  - old telegram-codex-app-bridge.service is inactive/disabled if present
53
- - npm run status works
54
+ - foxclaw status works
54
55
  10. Report the commands used, the final status, and the log command I should use if something stops working. Redact TG_BOT_TOKEN and never print the full token or full .env content.
55
56
  ```
56
57
 
@@ -61,18 +62,19 @@ Use this when the target computer is still running `telegram-codex-app-bridge`:
61
62
  ```text
62
63
  Migrate this machine from telegram-codex-app-bridge to FoxClaw.
63
64
 
64
- New repository:
65
- https://github.com/foxden-app/foxclaw.git
65
+ New package:
66
+ @foxden-app/foxclaw
66
67
 
67
68
  Please:
68
- 1. Check git status before changing the repo. If there are uncommitted local changes, stop and report them.
69
+ 1. Inspect the current install method, service file, and runtime directory before changing anything.
69
70
  2. Stop and disable telegram-codex-app-bridge.service if it exists.
70
71
  3. If ~/.foxclaw does not exist and ~/.telegram-codex-app-bridge exists, copy ~/.telegram-codex-app-bridge to ~/.foxclaw.
71
- 4. Update the source repo to https://github.com/foxden-app/foxclaw.git and pull main.
72
- 5. Run npm install, npm run build, npm run doctor.
73
- 6. Install and start foxclaw.service with npm run install-systemd.
74
- 7. Verify foxclaw.service is active and telegram-codex-app-bridge.service is inactive/disabled.
75
- 8. Report final status and any blockers. Redact TG_BOT_TOKEN and never print the full token or full .env content.
72
+ 4. Install or update FoxClaw with npm install -g @foxden-app/foxclaw@latest.
73
+ 5. Run foxclaw init if ~/.foxclaw/.env does not exist, then verify ~/.foxclaw/.env.
74
+ 6. Run foxclaw doctor.
75
+ 7. Install and start foxclaw.service with foxclaw install-systemd.
76
+ 8. Verify foxclaw.service is active and telegram-codex-app-bridge.service is inactive/disabled.
77
+ 9. Report final status and any blockers. Redact TG_BOT_TOKEN and never print the full token or full .env content.
76
78
  ```
77
79
 
78
80
  ## Safety Notes
@@ -114,15 +114,22 @@ The easiest path:
114
114
 
115
115
  Use the number, not your `@username`.
116
116
 
117
- ## 6. Download FoxClaw
117
+ ## 6. Install FoxClaw
118
118
 
119
- Choose a stable folder and clone the repo:
119
+ Install the published npm package:
120
120
 
121
121
  ```bash
122
- git clone https://github.com/foxden-app/foxclaw.git
123
- cd foxclaw
124
- npm install
125
- cp .env.example .env
122
+ npm install -g @foxden-app/foxclaw
123
+ foxclaw init
124
+ ```
125
+
126
+ This creates the config file at `~/.foxclaw/.env`.
127
+
128
+ If you prefer pnpm:
129
+
130
+ ```bash
131
+ pnpm add -g @foxden-app/foxclaw
132
+ foxclaw init
126
133
  ```
127
134
 
128
135
  ## 7. Fill In `.env`
@@ -130,7 +137,7 @@ cp .env.example .env
130
137
  Open `.env` in a simple editor:
131
138
 
132
139
  ```bash
133
- nano .env
140
+ nano ~/.foxclaw/.env
134
141
  ```
135
142
 
136
143
  For a first private-chat install, fill only the important values:
@@ -158,11 +165,10 @@ In `nano`, press `Ctrl+O`, Enter, then `Ctrl+X` to save and exit.
158
165
 
159
166
  ## 8. Run The First Check
160
167
 
161
- Build and run doctor:
168
+ Run doctor:
162
169
 
163
170
  ```bash
164
- npm run build
165
- npm run doctor
171
+ foxclaw doctor
166
172
  ```
167
173
 
168
174
  You want to see:
@@ -182,7 +188,7 @@ If you see `[FAIL]`, stop and check [Troubleshooting](./troubleshooting.md).
182
188
  Run FoxClaw directly before installing it as a background service:
183
189
 
184
190
  ```bash
185
- npm run serve
191
+ foxclaw serve
186
192
  ```
187
193
 
188
194
  Leave this terminal open.
@@ -232,7 +238,7 @@ Only do this after foreground mode works.
232
238
  On Linux with systemd:
233
239
 
234
240
  ```bash
235
- npm run install-systemd
241
+ foxclaw install-systemd
236
242
  systemctl --user status foxclaw.service
237
243
  journalctl --user -u foxclaw.service -f
238
244
  ```
@@ -246,7 +252,7 @@ loginctl enable-linger "$USER"
246
252
  On macOS:
247
253
 
248
254
  ```bash
249
- ./scripts/launchd/install.sh
255
+ foxclaw install-launchd
250
256
  ```
251
257
 
252
258
  launchd starts FoxClaw when you log in.
@@ -256,7 +262,7 @@ launchd starts FoxClaw when you log in.
256
262
  Check current status:
257
263
 
258
264
  ```bash
259
- npm run status
265
+ foxclaw status
260
266
  ```
261
267
 
262
268
  Restart Linux service after changing `.env`:
@@ -274,14 +280,12 @@ systemctl --user stop foxclaw.service
274
280
  Uninstall Linux service:
275
281
 
276
282
  ```bash
277
- npm run uninstall-systemd
283
+ foxclaw uninstall-systemd
278
284
  ```
279
285
 
280
286
  Update FoxClaw later:
281
287
 
282
288
  ```bash
283
- git pull
284
- npm install
285
- npm run build
289
+ npm install -g @foxden-app/foxclaw@latest
286
290
  systemctl --user restart foxclaw.service
287
291
  ```
@@ -1,10 +1,10 @@
1
1
  # Troubleshooting
2
2
 
3
- Start with these commands from the FoxClaw repo directory:
3
+ Start with these commands:
4
4
 
5
5
  ```bash
6
- npm run doctor
7
- npm run status
6
+ foxclaw doctor
7
+ foxclaw status
8
8
  ```
9
9
 
10
10
  If FoxClaw is installed as a Linux user service, also check:
@@ -18,7 +18,7 @@ journalctl --user -u foxclaw.service -f
18
18
 
19
19
  | Symptom | Meaning | Fix |
20
20
  | --- | --- | --- |
21
- | `[FAIL] node >= 24` | Your current shell is using an older Node.js. | Run `nvm install 24 && nvm use 24`, then rerun `npm run doctor`. If the service uses old Node, reinstall it from a Node 24 shell with `npm run install-systemd`. |
21
+ | `[FAIL] node >= 24` | Your current shell is using an older Node.js. | Run `nvm install 24 && nvm use 24`, then rerun `foxclaw doctor`. If the service uses old Node, reinstall it from a Node 24 shell with `foxclaw install-systemd`. |
22
22
  | `[FAIL] codex cli available` | The `codex` command is not in PATH. | Install Codex CLI or fix PATH, then confirm `codex --version` works. |
23
23
  | `[FAIL] telegram bot token configured` | `TG_BOT_TOKEN` is missing from `.env`. | Copy the token from `@BotFather` into `.env`. |
24
24
  | `[FAIL] telegram allowed user configured` | `TG_ALLOWED_USER_ID` is missing from `.env`. | Get your numeric id from `@userinfobot` and add it to `.env`. |
@@ -77,7 +77,7 @@ Check these in order:
77
77
  1. Make sure FoxClaw is running:
78
78
 
79
79
  ```bash
80
- npm run status
80
+ foxclaw status
81
81
  ```
82
82
 
83
83
  2. Try private chat first. Open your bot directly and send:
@@ -96,7 +96,7 @@ Check these in order:
96
96
  systemctl --user restart foxclaw.service
97
97
  ```
98
98
 
99
- If running foreground mode, stop with `Ctrl+C` and run `npm run serve` again.
99
+ If running foreground mode, stop with `Ctrl+C` and run `foxclaw serve` again.
100
100
 
101
101
  ## Group Messages Do Not Work
102
102
 
@@ -194,7 +194,7 @@ The systemd installer captures the `node` binary from your current PATH. If you
194
194
 
195
195
  ```bash
196
196
  nvm use 24
197
- npm run install-systemd
197
+ foxclaw install-systemd
198
198
  systemctl --user status foxclaw.service
199
199
  ```
200
200
 
@@ -217,7 +217,7 @@ loginctl enable-linger "$USER"
217
217
  macOS launchd starts FoxClaw when you log in after running:
218
218
 
219
219
  ```bash
220
- ./scripts/launchd/install.sh
220
+ foxclaw install-launchd
221
221
  ```
222
222
 
223
223
  ## Migrating From The Old Project Name
@@ -227,13 +227,10 @@ If this machine still runs `telegram-codex-app-bridge`, migrate once:
227
227
  ```bash
228
228
  systemctl --user disable --now telegram-codex-app-bridge.service 2>/dev/null || true
229
229
  test -e ~/.foxclaw || cp -a ~/.telegram-codex-app-bridge ~/.foxclaw
230
- git remote set-url origin https://github.com/foxden-app/foxclaw.git
231
- git fetch origin main
232
- git checkout main
233
- git pull --ff-only origin main
234
- npm install
235
- npm run build
236
- npm run install-systemd
230
+ npm install -g @foxden-app/foxclaw@latest
231
+ foxclaw init
232
+ foxclaw doctor
233
+ foxclaw install-systemd
237
234
  ```
238
235
 
239
- If `git status --short` shows local changes before migration, stop and review those changes before pulling.
236
+ If `~/.foxclaw/.env` already exists, `foxclaw init` leaves it untouched.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -8,13 +8,13 @@
8
8
  "foxclaw": "dist/main.js"
9
9
  },
10
10
  "files": [
11
- "dist",
12
- "docs",
13
- "scripts",
11
+ "dist",
12
+ "docs",
13
+ "scripts",
14
14
  "skills",
15
15
  ".env.example",
16
- "README.md",
17
- "README_EN.md",
16
+ "README.md",
17
+ "README_EN.md",
18
18
  "LICENSE"
19
19
  ],
20
20
  "private": false,
@@ -32,21 +32,23 @@
32
32
  "engines": {
33
33
  "node": ">=24"
34
34
  },
35
- "scripts": {
36
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
37
- "build": "npm run clean && tsc -p tsconfig.build.json",
38
- "dev": "tsx src/main.ts serve",
39
- "serve": "node dist/main.js serve",
35
+ "scripts": {
36
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
37
+ "build": "npm run clean && tsc -p tsconfig.build.json",
38
+ "dev": "tsx src/main.ts serve",
39
+ "serve": "node dist/main.js serve",
40
40
  "weixin-login": "node dist/main.js weixin-login",
41
41
  "status": "node dist/main.js status",
42
42
  "doctor": "node dist/main.js doctor",
43
- "install-systemd": "bash scripts/systemd/install.sh",
44
- "uninstall-systemd": "bash scripts/systemd/uninstall.sh",
43
+ "init": "node dist/main.js init",
44
+ "install-systemd": "node dist/main.js install-systemd",
45
+ "uninstall-systemd": "node dist/main.js uninstall-systemd",
46
+ "install-launchd": "node dist/main.js install-launchd",
45
47
  "typecheck": "tsc --noEmit",
46
48
  "lint": "eslint .",
47
- "test": "node --test --import tsx \"src/**/*.test.ts\"",
48
- "prepack": "npm run build"
49
- },
49
+ "test": "node --test --import tsx \"src/**/*.test.ts\"",
50
+ "prepack": "npm run build"
51
+ },
50
52
  "dependencies": {
51
53
  "dotenv": "^16.6.1",
52
54
  "qrcode-terminal": "^0.12.0"
package/scripts/doctor.sh CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- node dist/main.js doctor
3
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
4
+ node "$ROOT_DIR/dist/main.js" doctor
@@ -1,54 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
4
- PLIST="$HOME/Library/LaunchAgents/app.foxden.foxclaw.plist"
5
- NODE_BIN="$(command -v node)"
6
- PATH_VALUE="$PATH"
7
- HOME_VALUE="$HOME"
8
- USER_VALUE="${USER:-}"
9
- LOGNAME_VALUE="${LOGNAME:-$USER_VALUE}"
10
- if [[ -z "$NODE_BIN" ]]; then
11
- echo "node not found in PATH" >&2
12
- exit 1
13
- fi
14
- mkdir -p "$HOME/Library/LaunchAgents" "$HOME/.foxclaw/logs"
15
- cat > "$PLIST" <<PLIST
16
- <?xml version="1.0" encoding="UTF-8"?>
17
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18
- <plist version="1.0">
19
- <dict>
20
- <key>Label</key>
21
- <string>app.foxden.foxclaw</string>
22
- <key>ProgramArguments</key>
23
- <array>
24
- <string>$NODE_BIN</string>
25
- <string>$ROOT_DIR/dist/main.js</string>
26
- <string>serve</string>
27
- </array>
28
- <key>WorkingDirectory</key>
29
- <string>$ROOT_DIR</string>
30
- <key>EnvironmentVariables</key>
31
- <dict>
32
- <key>PATH</key>
33
- <string>$PATH_VALUE</string>
34
- <key>HOME</key>
35
- <string>$HOME_VALUE</string>
36
- <key>USER</key>
37
- <string>$USER_VALUE</string>
38
- <key>LOGNAME</key>
39
- <string>$LOGNAME_VALUE</string>
40
- </dict>
41
- <key>RunAtLoad</key>
42
- <true/>
43
- <key>KeepAlive</key>
44
- <true/>
45
- <key>StandardOutPath</key>
46
- <string>$HOME/.foxclaw/logs/launchd.out.log</string>
47
- <key>StandardErrorPath</key>
48
- <string>$HOME/.foxclaw/logs/launchd.err.log</string>
49
- </dict>
50
- </plist>
51
- PLIST
52
- launchctl unload "$PLIST" >/dev/null 2>&1 || true
53
- launchctl load "$PLIST"
54
- echo "Installed $PLIST"
3
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
4
+ node "$ROOT_DIR/dist/main.js" install-launchd
package/scripts/status.sh CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- node dist/main.js status
3
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
4
+ node "$ROOT_DIR/dist/main.js" status
@@ -1,83 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
4
- UNIT_NAME="foxclaw.service"
5
- USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
6
- UNIT_PATH="$USER_SYSTEMD_DIR/$UNIT_NAME"
7
- NODE_BIN="$(command -v node)"
8
- # Do not embed full interactive $PATH: WSL often includes /mnt/c/Program Files/... which breaks
9
- # systemd's unquoted Environment=PATH=... (spaces split the assignment). Use a small Linux PATH.
10
- NODE_DIR="$(dirname "$NODE_BIN")"
11
- PATH_VALUE="${NODE_DIR}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
12
- if [[ -d "${HOME}/.local/bin" ]]; then
13
- PATH_VALUE="${HOME}/.local/bin:${PATH_VALUE}"
14
- fi
15
- HOME_VALUE="${HOME:?}"
16
- USER_VALUE="${USER:-}"
17
- LOGNAME_VALUE="${LOGNAME:-$USER_VALUE}"
18
-
19
- if [[ -z "$NODE_BIN" ]]; then
20
- echo "node not found in PATH" >&2
21
- exit 1
22
- fi
23
-
24
- if ! command -v systemctl >/dev/null 2>&1; then
25
- echo "systemctl not found (need systemd)" >&2
26
- exit 1
27
- fi
28
-
29
- NO_BUILD=0
30
- NO_START=0
31
- for arg in "$@"; do
32
- case "$arg" in
33
- --no-build) NO_BUILD=1 ;;
34
- --no-start) NO_START=1 ;;
35
- esac
36
- done
37
-
38
- if [[ "$NO_BUILD" -eq 0 ]]; then
39
- (cd "$ROOT_DIR" && npm run build)
40
- fi
41
-
42
- mkdir -p "$USER_SYSTEMD_DIR" "$HOME/.foxclaw/logs"
43
-
44
- cat >"$UNIT_PATH" <<UNIT
45
- [Unit]
46
- Description=FoxClaw local Codex execution bridge
47
- Documentation=https://github.com/foxden-app/foxclaw
48
- After=network-online.target
49
- Wants=network-online.target
50
- StartLimitIntervalSec=300
51
- StartLimitBurst=5
52
-
53
- [Service]
54
- Type=simple
55
- WorkingDirectory=$ROOT_DIR
56
- Environment=HOME=$HOME_VALUE
57
- Environment=USER=$USER_VALUE
58
- Environment=LOGNAME=$LOGNAME_VALUE
59
- Environment=PATH=$PATH_VALUE
60
- ExecStart=$NODE_BIN dist/main.js serve
61
- Restart=always
62
- RestartSec=10
63
- TimeoutStopSec=45
64
- KillMode=process
65
-
66
- [Install]
67
- WantedBy=default.target
68
- UNIT
69
-
70
- systemctl --user daemon-reload
71
- systemctl --user enable "$UNIT_NAME"
72
- if [[ "$NO_START" -eq 0 ]]; then
73
- systemctl --user restart "$UNIT_NAME" || systemctl --user start "$UNIT_NAME"
74
- fi
75
-
76
- echo "Installed $UNIT_PATH"
77
- echo "Status: systemctl --user status $UNIT_NAME"
78
- echo "Logs: journalctl --user -u $UNIT_NAME -f"
79
- if [[ "$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || true)" != "yes" ]]; then
80
- echo ""
81
- echo "Tip: for this service to start at boot without an interactive login, run once:"
82
- echo " loginctl enable-linger $USER"
83
- fi
4
+ node "$ROOT_DIR/dist/main.js" install-systemd
@@ -1,15 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- UNIT_NAME="foxclaw.service"
4
- USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
5
- UNIT_PATH="$USER_SYSTEMD_DIR/$UNIT_NAME"
6
-
7
- if ! command -v systemctl >/dev/null 2>&1; then
8
- echo "systemctl not found" >&2
9
- exit 1
10
- fi
11
-
12
- systemctl --user disable --now "$UNIT_NAME" 2>/dev/null || true
13
- rm -f "$UNIT_PATH"
14
- systemctl --user daemon-reload
15
- echo "Removed $UNIT_PATH"
3
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
4
+ node "$ROOT_DIR/dist/main.js" uninstall-systemd