@rozek/nanoclaw 1.2.17
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/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +290 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package-lock.json +1524 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +558 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1738 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +467 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +34 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +263 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +102 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +190 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +210 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +12 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1856 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +707 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +365 -0
- package/src/index.ts +731 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +295 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +107 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
package/setup/service.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step: service — Generate and load service manager config.
|
|
3
|
+
* Replaces 08-setup-service.sh
|
|
4
|
+
*
|
|
5
|
+
* Fixes: Root→system systemd, WSL nohup fallback, no `|| true` swallowing errors.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
import { logger } from '../src/logger.js';
|
|
13
|
+
import {
|
|
14
|
+
getPlatform,
|
|
15
|
+
getNodePath,
|
|
16
|
+
getServiceManager,
|
|
17
|
+
hasSystemd,
|
|
18
|
+
isRoot,
|
|
19
|
+
isWSL,
|
|
20
|
+
} from './platform.js';
|
|
21
|
+
import { emitStatus } from './status.js';
|
|
22
|
+
|
|
23
|
+
export async function run(_args: string[]): Promise<void> {
|
|
24
|
+
const projectRoot = process.cwd();
|
|
25
|
+
const platform = getPlatform();
|
|
26
|
+
const nodePath = getNodePath();
|
|
27
|
+
const homeDir = os.homedir();
|
|
28
|
+
|
|
29
|
+
logger.info({ platform, nodePath, projectRoot }, 'Setting up service');
|
|
30
|
+
|
|
31
|
+
// Build first
|
|
32
|
+
logger.info('Building TypeScript');
|
|
33
|
+
try {
|
|
34
|
+
execSync('npm run build', {
|
|
35
|
+
cwd: projectRoot,
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
logger.info('Build succeeded');
|
|
39
|
+
} catch {
|
|
40
|
+
logger.error('Build failed');
|
|
41
|
+
emitStatus('SETUP_SERVICE', {
|
|
42
|
+
SERVICE_TYPE: 'unknown',
|
|
43
|
+
NODE_PATH: nodePath,
|
|
44
|
+
PROJECT_PATH: projectRoot,
|
|
45
|
+
STATUS: 'failed',
|
|
46
|
+
ERROR: 'build_failed',
|
|
47
|
+
LOG: 'logs/setup.log',
|
|
48
|
+
});
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
|
53
|
+
|
|
54
|
+
if (platform === 'macos') {
|
|
55
|
+
setupLaunchd(projectRoot, nodePath, homeDir);
|
|
56
|
+
} else if (platform === 'linux') {
|
|
57
|
+
setupLinux(projectRoot, nodePath, homeDir);
|
|
58
|
+
} else {
|
|
59
|
+
emitStatus('SETUP_SERVICE', {
|
|
60
|
+
SERVICE_TYPE: 'unknown',
|
|
61
|
+
NODE_PATH: nodePath,
|
|
62
|
+
PROJECT_PATH: projectRoot,
|
|
63
|
+
STATUS: 'failed',
|
|
64
|
+
ERROR: 'unsupported_platform',
|
|
65
|
+
LOG: 'logs/setup.log',
|
|
66
|
+
});
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setupLaunchd(
|
|
72
|
+
projectRoot: string,
|
|
73
|
+
nodePath: string,
|
|
74
|
+
homeDir: string,
|
|
75
|
+
): void {
|
|
76
|
+
const plistPath = path.join(
|
|
77
|
+
homeDir,
|
|
78
|
+
'Library',
|
|
79
|
+
'LaunchAgents',
|
|
80
|
+
'com.nanoclaw.plist',
|
|
81
|
+
);
|
|
82
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
83
|
+
|
|
84
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
85
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
86
|
+
<plist version="1.0">
|
|
87
|
+
<dict>
|
|
88
|
+
<key>Label</key>
|
|
89
|
+
<string>com.nanoclaw</string>
|
|
90
|
+
<key>ProgramArguments</key>
|
|
91
|
+
<array>
|
|
92
|
+
<string>${nodePath}</string>
|
|
93
|
+
<string>${projectRoot}/dist/index.js</string>
|
|
94
|
+
</array>
|
|
95
|
+
<key>WorkingDirectory</key>
|
|
96
|
+
<string>${projectRoot}</string>
|
|
97
|
+
<key>RunAtLoad</key>
|
|
98
|
+
<true/>
|
|
99
|
+
<key>KeepAlive</key>
|
|
100
|
+
<true/>
|
|
101
|
+
<key>EnvironmentVariables</key>
|
|
102
|
+
<dict>
|
|
103
|
+
<key>PATH</key>
|
|
104
|
+
<string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
|
|
105
|
+
<key>HOME</key>
|
|
106
|
+
<string>${homeDir}</string>
|
|
107
|
+
</dict>
|
|
108
|
+
<key>StandardOutPath</key>
|
|
109
|
+
<string>${projectRoot}/logs/nanoclaw.log</string>
|
|
110
|
+
<key>StandardErrorPath</key>
|
|
111
|
+
<string>${projectRoot}/logs/nanoclaw.error.log</string>
|
|
112
|
+
</dict>
|
|
113
|
+
</plist>`;
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(plistPath, plist);
|
|
116
|
+
logger.info({ plistPath }, 'Wrote launchd plist');
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
execSync(`launchctl load ${JSON.stringify(plistPath)}`, {
|
|
120
|
+
stdio: 'ignore',
|
|
121
|
+
});
|
|
122
|
+
logger.info('launchctl load succeeded');
|
|
123
|
+
} catch {
|
|
124
|
+
logger.warn('launchctl load failed (may already be loaded)');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Verify
|
|
128
|
+
let serviceLoaded = false;
|
|
129
|
+
try {
|
|
130
|
+
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
|
131
|
+
serviceLoaded = output.includes('com.nanoclaw');
|
|
132
|
+
} catch {
|
|
133
|
+
// launchctl list failed
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
emitStatus('SETUP_SERVICE', {
|
|
137
|
+
SERVICE_TYPE: 'launchd',
|
|
138
|
+
NODE_PATH: nodePath,
|
|
139
|
+
PROJECT_PATH: projectRoot,
|
|
140
|
+
PLIST_PATH: plistPath,
|
|
141
|
+
SERVICE_LOADED: serviceLoaded,
|
|
142
|
+
STATUS: 'success',
|
|
143
|
+
LOG: 'logs/setup.log',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function setupLinux(
|
|
148
|
+
projectRoot: string,
|
|
149
|
+
nodePath: string,
|
|
150
|
+
homeDir: string,
|
|
151
|
+
): void {
|
|
152
|
+
const serviceManager = getServiceManager();
|
|
153
|
+
|
|
154
|
+
if (serviceManager === 'systemd') {
|
|
155
|
+
setupSystemd(projectRoot, nodePath, homeDir);
|
|
156
|
+
} else {
|
|
157
|
+
// WSL without systemd or other Linux without systemd
|
|
158
|
+
setupNohupFallback(projectRoot, nodePath, homeDir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Kill any orphaned nanoclaw node processes left from previous runs or debugging.
|
|
164
|
+
* Prevents connection conflicts when two instances connect to the same channel simultaneously.
|
|
165
|
+
*/
|
|
166
|
+
function killOrphanedProcesses(projectRoot: string): void {
|
|
167
|
+
try {
|
|
168
|
+
execSync(`pkill -f '${projectRoot}/dist/index\\.js' || true`, {
|
|
169
|
+
stdio: 'ignore',
|
|
170
|
+
});
|
|
171
|
+
logger.info('Stopped any orphaned nanoclaw processes');
|
|
172
|
+
} catch {
|
|
173
|
+
// pkill not available or no orphans
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Detect stale docker group membership in the user systemd session.
|
|
179
|
+
*
|
|
180
|
+
* When a user is added to the docker group mid-session, the user systemd
|
|
181
|
+
* daemon (user@UID.service) keeps the old group list from login time.
|
|
182
|
+
* Docker works in the terminal but not in the service context.
|
|
183
|
+
*
|
|
184
|
+
* Only relevant on Linux with user-level systemd (not root, not macOS, not WSL nohup).
|
|
185
|
+
*/
|
|
186
|
+
function checkDockerGroupStale(): boolean {
|
|
187
|
+
try {
|
|
188
|
+
execSync('systemd-run --user --pipe --wait docker info', {
|
|
189
|
+
stdio: 'pipe',
|
|
190
|
+
timeout: 10000,
|
|
191
|
+
});
|
|
192
|
+
return false; // Docker works from systemd session
|
|
193
|
+
} catch {
|
|
194
|
+
// Check if docker works from the current shell (to distinguish stale group vs broken docker)
|
|
195
|
+
try {
|
|
196
|
+
execSync('docker info', { stdio: 'pipe', timeout: 5000 });
|
|
197
|
+
return true; // Works in shell but not systemd session → stale group
|
|
198
|
+
} catch {
|
|
199
|
+
return false; // Docker itself is not working, different issue
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function setupSystemd(
|
|
205
|
+
projectRoot: string,
|
|
206
|
+
nodePath: string,
|
|
207
|
+
homeDir: string,
|
|
208
|
+
): void {
|
|
209
|
+
const runningAsRoot = isRoot();
|
|
210
|
+
|
|
211
|
+
// Root uses system-level service, non-root uses user-level
|
|
212
|
+
let unitPath: string;
|
|
213
|
+
let systemctlPrefix: string;
|
|
214
|
+
|
|
215
|
+
if (runningAsRoot) {
|
|
216
|
+
unitPath = '/etc/systemd/system/nanoclaw.service';
|
|
217
|
+
systemctlPrefix = 'systemctl';
|
|
218
|
+
logger.info('Running as root — installing system-level systemd unit');
|
|
219
|
+
} else {
|
|
220
|
+
// Check if user-level systemd session is available
|
|
221
|
+
try {
|
|
222
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
223
|
+
} catch {
|
|
224
|
+
logger.warn(
|
|
225
|
+
'systemd user session not available — falling back to nohup wrapper',
|
|
226
|
+
);
|
|
227
|
+
setupNohupFallback(projectRoot, nodePath, homeDir);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const unitDir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
231
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
232
|
+
unitPath = path.join(unitDir, 'nanoclaw.service');
|
|
233
|
+
systemctlPrefix = 'systemctl --user';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const unit = `[Unit]
|
|
237
|
+
Description=NanoClaw Personal Assistant
|
|
238
|
+
After=network.target
|
|
239
|
+
|
|
240
|
+
[Service]
|
|
241
|
+
Type=simple
|
|
242
|
+
ExecStart=${nodePath} ${projectRoot}/dist/index.js
|
|
243
|
+
WorkingDirectory=${projectRoot}
|
|
244
|
+
Restart=always
|
|
245
|
+
RestartSec=5
|
|
246
|
+
KillMode=process
|
|
247
|
+
Environment=HOME=${homeDir}
|
|
248
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
|
|
249
|
+
StandardOutput=append:${projectRoot}/logs/nanoclaw.log
|
|
250
|
+
StandardError=append:${projectRoot}/logs/nanoclaw.error.log
|
|
251
|
+
|
|
252
|
+
[Install]
|
|
253
|
+
WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(unitPath, unit);
|
|
256
|
+
logger.info({ unitPath }, 'Wrote systemd unit');
|
|
257
|
+
|
|
258
|
+
// Detect stale docker group before starting (user systemd only)
|
|
259
|
+
const dockerGroupStale = !runningAsRoot && checkDockerGroupStale();
|
|
260
|
+
if (dockerGroupStale) {
|
|
261
|
+
logger.warn(
|
|
262
|
+
'Docker group not active in systemd session — user was likely added to docker group mid-session',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Kill orphaned nanoclaw processes to avoid channel connection conflicts
|
|
267
|
+
killOrphanedProcesses(projectRoot);
|
|
268
|
+
|
|
269
|
+
// Enable and start
|
|
270
|
+
try {
|
|
271
|
+
execSync(`${systemctlPrefix} daemon-reload`, { stdio: 'ignore' });
|
|
272
|
+
} catch (err) {
|
|
273
|
+
logger.error({ err }, 'systemctl daemon-reload failed');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
execSync(`${systemctlPrefix} enable nanoclaw`, { stdio: 'ignore' });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
logger.error({ err }, 'systemctl enable failed');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
execSync(`${systemctlPrefix} start nanoclaw`, { stdio: 'ignore' });
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logger.error({ err }, 'systemctl start failed');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Verify
|
|
289
|
+
let serviceLoaded = false;
|
|
290
|
+
try {
|
|
291
|
+
execSync(`${systemctlPrefix} is-active nanoclaw`, { stdio: 'ignore' });
|
|
292
|
+
serviceLoaded = true;
|
|
293
|
+
} catch {
|
|
294
|
+
// Not active
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
emitStatus('SETUP_SERVICE', {
|
|
298
|
+
SERVICE_TYPE: runningAsRoot ? 'systemd-system' : 'systemd-user',
|
|
299
|
+
NODE_PATH: nodePath,
|
|
300
|
+
PROJECT_PATH: projectRoot,
|
|
301
|
+
UNIT_PATH: unitPath,
|
|
302
|
+
SERVICE_LOADED: serviceLoaded,
|
|
303
|
+
...(dockerGroupStale ? { DOCKER_GROUP_STALE: true } : {}),
|
|
304
|
+
STATUS: 'success',
|
|
305
|
+
LOG: 'logs/setup.log',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function setupNohupFallback(
|
|
310
|
+
projectRoot: string,
|
|
311
|
+
nodePath: string,
|
|
312
|
+
homeDir: string,
|
|
313
|
+
): void {
|
|
314
|
+
logger.warn('No systemd detected — generating nohup wrapper script');
|
|
315
|
+
|
|
316
|
+
const wrapperPath = path.join(projectRoot, 'start-nanoclaw.sh');
|
|
317
|
+
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
|
|
318
|
+
|
|
319
|
+
const lines = [
|
|
320
|
+
'#!/bin/bash',
|
|
321
|
+
'# start-nanoclaw.sh — Start NanoClaw without systemd',
|
|
322
|
+
`# To stop: kill \\$(cat ${pidFile})`,
|
|
323
|
+
'',
|
|
324
|
+
'set -euo pipefail',
|
|
325
|
+
'',
|
|
326
|
+
`cd ${JSON.stringify(projectRoot)}`,
|
|
327
|
+
'',
|
|
328
|
+
'# Stop existing instance if running',
|
|
329
|
+
`if [ -f ${JSON.stringify(pidFile)} ]; then`,
|
|
330
|
+
` OLD_PID=$(cat ${JSON.stringify(pidFile)} 2>/dev/null || echo "")`,
|
|
331
|
+
' if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then',
|
|
332
|
+
' echo "Stopping existing NanoClaw (PID $OLD_PID)..."',
|
|
333
|
+
' kill "$OLD_PID" 2>/dev/null || true',
|
|
334
|
+
' sleep 2',
|
|
335
|
+
' fi',
|
|
336
|
+
'fi',
|
|
337
|
+
'',
|
|
338
|
+
'echo "Starting NanoClaw..."',
|
|
339
|
+
`nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot + '/dist/index.js')} \\`,
|
|
340
|
+
` >> ${JSON.stringify(projectRoot + '/logs/nanoclaw.log')} \\`,
|
|
341
|
+
` 2>> ${JSON.stringify(projectRoot + '/logs/nanoclaw.error.log')} &`,
|
|
342
|
+
'',
|
|
343
|
+
`echo $! > ${JSON.stringify(pidFile)}`,
|
|
344
|
+
'echo "NanoClaw started (PID $!)"',
|
|
345
|
+
`echo "Logs: tail -f ${projectRoot}/logs/nanoclaw.log"`,
|
|
346
|
+
];
|
|
347
|
+
const wrapper = lines.join('\n') + '\n';
|
|
348
|
+
|
|
349
|
+
fs.writeFileSync(wrapperPath, wrapper, { mode: 0o755 });
|
|
350
|
+
logger.info({ wrapperPath }, 'Wrote nohup wrapper script');
|
|
351
|
+
|
|
352
|
+
emitStatus('SETUP_SERVICE', {
|
|
353
|
+
SERVICE_TYPE: 'nohup',
|
|
354
|
+
NODE_PATH: nodePath,
|
|
355
|
+
PROJECT_PATH: projectRoot,
|
|
356
|
+
WRAPPER_PATH: wrapperPath,
|
|
357
|
+
SERVICE_LOADED: false,
|
|
358
|
+
FALLBACK: 'wsl_no_systemd',
|
|
359
|
+
STATUS: 'success',
|
|
360
|
+
LOG: 'logs/setup.log',
|
|
361
|
+
});
|
|
362
|
+
}
|
package/setup/status.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured status block output for setup steps.
|
|
3
|
+
* Each step emits a block that the SKILL.md LLM can parse.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function emitStatus(
|
|
7
|
+
step: string,
|
|
8
|
+
fields: Record<string, string | number | boolean>,
|
|
9
|
+
): void {
|
|
10
|
+
const lines = [`=== NANOCLAW SETUP: ${step} ===`];
|
|
11
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
12
|
+
lines.push(`${key}: ${value}`);
|
|
13
|
+
}
|
|
14
|
+
lines.push('=== END ===');
|
|
15
|
+
console.log(lines.join('\n'));
|
|
16
|
+
}
|
package/setup/verify.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step: verify — End-to-end health check of the full installation.
|
|
3
|
+
* Replaces 09-verify.sh
|
|
4
|
+
*
|
|
5
|
+
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
|
|
14
|
+
import { STORE_DIR } from '../src/config.js';
|
|
15
|
+
import { readEnvFile } from '../src/env.js';
|
|
16
|
+
import { logger } from '../src/logger.js';
|
|
17
|
+
import {
|
|
18
|
+
getPlatform,
|
|
19
|
+
getServiceManager,
|
|
20
|
+
hasSystemd,
|
|
21
|
+
isRoot,
|
|
22
|
+
} from './platform.js';
|
|
23
|
+
import { emitStatus } from './status.js';
|
|
24
|
+
|
|
25
|
+
export async function run(_args: string[]): Promise<void> {
|
|
26
|
+
const projectRoot = process.cwd();
|
|
27
|
+
const platform = getPlatform();
|
|
28
|
+
const homeDir = os.homedir();
|
|
29
|
+
|
|
30
|
+
logger.info('Starting verification');
|
|
31
|
+
|
|
32
|
+
// 1. Check service status
|
|
33
|
+
let service = 'not_found';
|
|
34
|
+
const mgr = getServiceManager();
|
|
35
|
+
|
|
36
|
+
if (mgr === 'launchd') {
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
|
39
|
+
if (output.includes('com.nanoclaw')) {
|
|
40
|
+
// Check if it has a PID (actually running)
|
|
41
|
+
const line = output.split('\n').find((l) => l.includes('com.nanoclaw'));
|
|
42
|
+
if (line) {
|
|
43
|
+
const pidField = line.trim().split(/\s+/)[0];
|
|
44
|
+
service = pidField !== '-' && pidField ? 'running' : 'stopped';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// launchctl not available
|
|
49
|
+
}
|
|
50
|
+
} else if (mgr === 'systemd') {
|
|
51
|
+
const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
|
|
52
|
+
try {
|
|
53
|
+
execSync(`${prefix} is-active nanoclaw`, { stdio: 'ignore' });
|
|
54
|
+
service = 'running';
|
|
55
|
+
} catch {
|
|
56
|
+
try {
|
|
57
|
+
const output = execSync(`${prefix} list-unit-files`, {
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
});
|
|
60
|
+
if (output.includes('nanoclaw')) {
|
|
61
|
+
service = 'stopped';
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// systemctl not available
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Check for nohup PID file
|
|
69
|
+
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
|
|
70
|
+
if (fs.existsSync(pidFile)) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = fs.readFileSync(pidFile, 'utf-8').trim();
|
|
73
|
+
const pid = Number(raw);
|
|
74
|
+
if (raw && Number.isInteger(pid) && pid > 0) {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
service = 'running';
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
service = 'stopped';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
logger.info({ service }, 'Service status');
|
|
84
|
+
|
|
85
|
+
// 2. Check container runtime
|
|
86
|
+
let containerRuntime = 'none';
|
|
87
|
+
try {
|
|
88
|
+
execSync('command -v container', { stdio: 'ignore' });
|
|
89
|
+
containerRuntime = 'apple-container';
|
|
90
|
+
} catch {
|
|
91
|
+
try {
|
|
92
|
+
execSync('docker info', { stdio: 'ignore' });
|
|
93
|
+
containerRuntime = 'docker';
|
|
94
|
+
} catch {
|
|
95
|
+
// No runtime
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Check credentials
|
|
100
|
+
let credentials = 'missing';
|
|
101
|
+
const envFile = path.join(projectRoot, '.env');
|
|
102
|
+
if (fs.existsSync(envFile)) {
|
|
103
|
+
const envContent = fs.readFileSync(envFile, 'utf-8');
|
|
104
|
+
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) {
|
|
105
|
+
credentials = 'configured';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Check channel auth (detect configured channels by credentials)
|
|
110
|
+
const envVars = readEnvFile([
|
|
111
|
+
'TELEGRAM_BOT_TOKEN',
|
|
112
|
+
'SLACK_BOT_TOKEN',
|
|
113
|
+
'SLACK_APP_TOKEN',
|
|
114
|
+
'DISCORD_BOT_TOKEN',
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const channelAuth: Record<string, string> = {};
|
|
118
|
+
|
|
119
|
+
// WhatsApp: check for auth credentials on disk
|
|
120
|
+
const authDir = path.join(projectRoot, 'store', 'auth');
|
|
121
|
+
if (fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0) {
|
|
122
|
+
channelAuth.whatsapp = 'authenticated';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Token-based channels: check .env
|
|
126
|
+
if (process.env.TELEGRAM_BOT_TOKEN || envVars.TELEGRAM_BOT_TOKEN) {
|
|
127
|
+
channelAuth.telegram = 'configured';
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
(process.env.SLACK_BOT_TOKEN || envVars.SLACK_BOT_TOKEN) &&
|
|
131
|
+
(process.env.SLACK_APP_TOKEN || envVars.SLACK_APP_TOKEN)
|
|
132
|
+
) {
|
|
133
|
+
channelAuth.slack = 'configured';
|
|
134
|
+
}
|
|
135
|
+
if (process.env.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) {
|
|
136
|
+
channelAuth.discord = 'configured';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const configuredChannels = Object.keys(channelAuth);
|
|
140
|
+
const anyChannelConfigured = configuredChannels.length > 0;
|
|
141
|
+
|
|
142
|
+
// 5. Check registered groups (using better-sqlite3, not sqlite3 CLI)
|
|
143
|
+
let registeredGroups = 0;
|
|
144
|
+
const dbPath = path.join(STORE_DIR, 'messages.db');
|
|
145
|
+
if (fs.existsSync(dbPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const db = new Database(dbPath, { readonly: true });
|
|
148
|
+
const row = db
|
|
149
|
+
.prepare('SELECT COUNT(*) as count FROM registered_groups')
|
|
150
|
+
.get() as { count: number };
|
|
151
|
+
registeredGroups = row.count;
|
|
152
|
+
db.close();
|
|
153
|
+
} catch {
|
|
154
|
+
// Table might not exist
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 6. Check mount allowlist
|
|
159
|
+
let mountAllowlist = 'missing';
|
|
160
|
+
if (
|
|
161
|
+
fs.existsSync(
|
|
162
|
+
path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'),
|
|
163
|
+
)
|
|
164
|
+
) {
|
|
165
|
+
mountAllowlist = 'configured';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Determine overall status
|
|
169
|
+
const status =
|
|
170
|
+
service === 'running' &&
|
|
171
|
+
credentials !== 'missing' &&
|
|
172
|
+
anyChannelConfigured &&
|
|
173
|
+
registeredGroups > 0
|
|
174
|
+
? 'success'
|
|
175
|
+
: 'failed';
|
|
176
|
+
|
|
177
|
+
logger.info({ status, channelAuth }, 'Verification complete');
|
|
178
|
+
|
|
179
|
+
emitStatus('VERIFY', {
|
|
180
|
+
SERVICE: service,
|
|
181
|
+
CONTAINER_RUNTIME: containerRuntime,
|
|
182
|
+
CREDENTIALS: credentials,
|
|
183
|
+
CONFIGURED_CHANNELS: configuredChannels.join(','),
|
|
184
|
+
CHANNEL_AUTH: JSON.stringify(channelAuth),
|
|
185
|
+
REGISTERED_GROUPS: registeredGroups,
|
|
186
|
+
MOUNT_ALLOWLIST: mountAllowlist,
|
|
187
|
+
STATUS: status,
|
|
188
|
+
LOG: 'logs/setup.log',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (status === 'failed') process.exit(1);
|
|
192
|
+
}
|