twinclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/bin/npm-twinclaw.js +17 -0
- package/bin/run-twinbot-cli.js +36 -0
- package/bin/twinbot.js +4 -0
- package/bin/twinclaw.js +4 -0
- package/dist/api/handlers/browser.js +160 -0
- package/dist/api/handlers/callback.js +80 -0
- package/dist/api/handlers/config-validate.js +19 -0
- package/dist/api/handlers/health.js +117 -0
- package/dist/api/handlers/local-state-backup.js +118 -0
- package/dist/api/handlers/persona-state.js +59 -0
- package/dist/api/handlers/skill-packages.js +94 -0
- package/dist/api/router.js +278 -0
- package/dist/api/runtime-event-producer.js +99 -0
- package/dist/api/shared.js +82 -0
- package/dist/api/websocket-hub.js +305 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/env-schema.js +202 -0
- package/dist/config/env-validator.js +223 -0
- package/dist/config/identity-bootstrap.js +115 -0
- package/dist/config/json-config.js +344 -0
- package/dist/config/workspace.js +186 -0
- package/dist/core/channels-cli.js +77 -0
- package/dist/core/cli.js +119 -0
- package/dist/core/context-assembly.js +33 -0
- package/dist/core/doctor.js +365 -0
- package/dist/core/gateway-cli.js +323 -0
- package/dist/core/gateway.js +416 -0
- package/dist/core/heartbeat.js +54 -0
- package/dist/core/install-cli.js +320 -0
- package/dist/core/lane-executor.js +134 -0
- package/dist/core/logs-cli.js +70 -0
- package/dist/core/onboarding.js +760 -0
- package/dist/core/pairing-cli.js +78 -0
- package/dist/core/secret-vault-cli.js +204 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +404 -0
- package/dist/interfaces/dispatcher.js +214 -0
- package/dist/interfaces/telegram_handler.js +82 -0
- package/dist/interfaces/tui-dashboard.js +53 -0
- package/dist/interfaces/whatsapp_handler.js +94 -0
- package/dist/release/cli.js +97 -0
- package/dist/release/mvp-gate-cli.js +118 -0
- package/dist/release/twinbot-config-schema.js +162 -0
- package/dist/release/twinclaw-config-schema.js +162 -0
- package/dist/services/block-chunker.js +174 -0
- package/dist/services/browser-service.js +334 -0
- package/dist/services/context-lifecycle.js +314 -0
- package/dist/services/db.js +1055 -0
- package/dist/services/delivery-tracker.js +110 -0
- package/dist/services/dm-pairing.js +245 -0
- package/dist/services/embedding-service.js +125 -0
- package/dist/services/file-watcher.js +125 -0
- package/dist/services/inbound-debounce.js +92 -0
- package/dist/services/incident-manager.js +516 -0
- package/dist/services/job-scheduler.js +176 -0
- package/dist/services/local-state-backup.js +682 -0
- package/dist/services/mcp-client-adapter.js +291 -0
- package/dist/services/mcp-server-manager.js +143 -0
- package/dist/services/model-router.js +927 -0
- package/dist/services/mvp-gate.js +845 -0
- package/dist/services/orchestration-service.js +422 -0
- package/dist/services/persona-state.js +256 -0
- package/dist/services/policy-engine.js +92 -0
- package/dist/services/proactive-notifier.js +94 -0
- package/dist/services/queue-service.js +146 -0
- package/dist/services/release-pipeline.js +652 -0
- package/dist/services/runtime-budget-governor.js +415 -0
- package/dist/services/secret-vault.js +704 -0
- package/dist/services/semantic-memory.js +249 -0
- package/dist/services/skill-package-manager.js +806 -0
- package/dist/services/skill-registry.js +122 -0
- package/dist/services/streaming-output.js +75 -0
- package/dist/services/stt-service.js +39 -0
- package/dist/services/tts-service.js +44 -0
- package/dist/skills/builtin.js +250 -0
- package/dist/skills/shell.js +87 -0
- package/dist/skills/types.js +1 -0
- package/dist/types/api.js +1 -0
- package/dist/types/context-budget.js +1 -0
- package/dist/types/doctor.js +1 -0
- package/dist/types/file-watcher.js +1 -0
- package/dist/types/incident.js +1 -0
- package/dist/types/local-state-backup.js +1 -0
- package/dist/types/mcp.js +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/model-routing.js +1 -0
- package/dist/types/mvp-gate.js +2 -0
- package/dist/types/orchestration.js +1 -0
- package/dist/types/persona-state.js +22 -0
- package/dist/types/policy.js +1 -0
- package/dist/types/reasoning-graph.js +1 -0
- package/dist/types/release.js +1 -0
- package/dist/types/reliability.js +1 -0
- package/dist/types/runtime-budget.js +1 -0
- package/dist/types/scheduler.js +1 -0
- package/dist/types/secret-vault.js +1 -0
- package/dist/types/skill-packages.js +1 -0
- package/dist/types/websocket.js +14 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/secret-scan.js +208 -0
- package/mcp-servers.json +179 -0
- package/package.json +81 -0
- package/skill-packages.json +92 -0
- package/skill-packages.lock.json +5 -0
- package/src/skills/builtin.ts +275 -0
- package/src/skills/shell.ts +118 -0
- package/src/skills/types.ts +30 -0
- package/src/types/api.ts +252 -0
- package/src/types/blessed-contrib.d.ts +4 -0
- package/src/types/context-budget.ts +76 -0
- package/src/types/doctor.ts +29 -0
- package/src/types/file-watcher.ts +26 -0
- package/src/types/incident.ts +57 -0
- package/src/types/local-state-backup.ts +121 -0
- package/src/types/mcp.ts +106 -0
- package/src/types/messaging.ts +35 -0
- package/src/types/model-routing.ts +61 -0
- package/src/types/mvp-gate.ts +99 -0
- package/src/types/orchestration.ts +65 -0
- package/src/types/persona-state.ts +61 -0
- package/src/types/policy.ts +27 -0
- package/src/types/reasoning-graph.ts +58 -0
- package/src/types/release.ts +115 -0
- package/src/types/reliability.ts +43 -0
- package/src/types/runtime-budget.ts +85 -0
- package/src/types/scheduler.ts +47 -0
- package/src/types/secret-vault.ts +62 -0
- package/src/types/skill-packages.ts +81 -0
- package/src/types/sqlite-vec.d.ts +5 -0
- package/src/types/websocket.ts +122 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
const TWINBOT_HOME = '.twinbot';
|
|
5
|
+
const LEGACY_TWINCLAW_HOME = '.twinclaw';
|
|
6
|
+
export function getProfileName() {
|
|
7
|
+
const profile = process.env.TWINBOT_PROFILE || process.env.TWINCLAW_PROFILE;
|
|
8
|
+
if (profile && profile.trim().length > 0) {
|
|
9
|
+
return profile.trim();
|
|
10
|
+
}
|
|
11
|
+
return 'default';
|
|
12
|
+
}
|
|
13
|
+
export function getWorkspaceDir() {
|
|
14
|
+
const profile = getProfileName();
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
if (profile === 'default') {
|
|
17
|
+
return path.join(home, TWINBOT_HOME, 'workspace');
|
|
18
|
+
}
|
|
19
|
+
const sanitizedProfile = profile.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
20
|
+
return path.join(home, TWINBOT_HOME, `workspace-${sanitizedProfile}`);
|
|
21
|
+
}
|
|
22
|
+
export function getWorkspaceSubdir(subdir) {
|
|
23
|
+
return path.join(getWorkspaceDir(), subdir);
|
|
24
|
+
}
|
|
25
|
+
export function getConfigPath() {
|
|
26
|
+
return path.join(getWorkspaceDir(), 'twinbot.json');
|
|
27
|
+
}
|
|
28
|
+
export function getDatabasePath() {
|
|
29
|
+
return path.join(getWorkspaceDir(), 'memory', 'twinbot.db');
|
|
30
|
+
}
|
|
31
|
+
export function getIdentityDir() {
|
|
32
|
+
return path.join(getWorkspaceDir(), 'identity');
|
|
33
|
+
}
|
|
34
|
+
export function getSecretsVaultPath() {
|
|
35
|
+
return path.join(getWorkspaceDir(), 'secrets.sqlite');
|
|
36
|
+
}
|
|
37
|
+
export function getTranscriptsDir() {
|
|
38
|
+
return path.join(getWorkspaceDir(), 'transcripts');
|
|
39
|
+
}
|
|
40
|
+
export function ensureWorkspaceDir() {
|
|
41
|
+
const workspaceDir = getWorkspaceDir();
|
|
42
|
+
if (!fs.existsSync(workspaceDir)) {
|
|
43
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function ensureWorkspaceSubdirs() {
|
|
47
|
+
ensureWorkspaceDir();
|
|
48
|
+
const subdirs = ['memory', 'identity', 'transcripts'];
|
|
49
|
+
for (const subdir of subdirs) {
|
|
50
|
+
const fullPath = getWorkspaceSubdir(subdir);
|
|
51
|
+
if (!fs.existsSync(fullPath)) {
|
|
52
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function getLegacyConfigPath() {
|
|
57
|
+
// 1. Check for twinbot.json in the new workspace first (partial migration state)
|
|
58
|
+
const newWorkspaceLegacyFile = path.join(getWorkspaceDir(), 'twinbot.json');
|
|
59
|
+
if (fs.existsSync(newWorkspaceLegacyFile)) {
|
|
60
|
+
return newWorkspaceLegacyFile;
|
|
61
|
+
}
|
|
62
|
+
// 2. Then check the old .twinbot home (flat structure)
|
|
63
|
+
const oldTwinBotFile = path.join(os.homedir(), TWINBOT_HOME, 'twinbot.json');
|
|
64
|
+
if (fs.existsSync(oldTwinBotFile)) {
|
|
65
|
+
return oldTwinBotFile;
|
|
66
|
+
}
|
|
67
|
+
// 3. Finally, check the legacy .twinclaw home
|
|
68
|
+
const legacyTwinClawFile = path.join(os.homedir(), LEGACY_TWINCLAW_HOME, 'twinclaw.json');
|
|
69
|
+
return legacyTwinClawFile;
|
|
70
|
+
}
|
|
71
|
+
export function hasLegacyConfig() {
|
|
72
|
+
const legacyPath = getLegacyConfigPath();
|
|
73
|
+
const workspacePath = getConfigPath();
|
|
74
|
+
return fs.existsSync(legacyPath) && !fs.existsSync(workspacePath);
|
|
75
|
+
}
|
|
76
|
+
export function migrateLegacyConfig() {
|
|
77
|
+
const legacyPath = getLegacyConfigPath();
|
|
78
|
+
const workspacePath = getConfigPath();
|
|
79
|
+
if (!fs.existsSync(legacyPath)) {
|
|
80
|
+
return { migrated: false, sourcePath: null, targetPath: null };
|
|
81
|
+
}
|
|
82
|
+
if (fs.existsSync(workspacePath)) {
|
|
83
|
+
return { migrated: false, sourcePath: legacyPath, targetPath: workspacePath };
|
|
84
|
+
}
|
|
85
|
+
const workspaceDir = getWorkspaceDir();
|
|
86
|
+
if (!fs.existsSync(workspaceDir)) {
|
|
87
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const content = fs.readFileSync(legacyPath, 'utf-8');
|
|
91
|
+
fs.writeFileSync(workspacePath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
92
|
+
const legacyDir = path.dirname(legacyPath);
|
|
93
|
+
const legacyFiles = ['identity', 'memory', 'transcripts', 'secrets.sqlite', 'twinbot.db', 'twinclaw.db'];
|
|
94
|
+
for (const file of legacyFiles) {
|
|
95
|
+
const legacyFile = path.join(legacyDir, file);
|
|
96
|
+
if (fs.existsSync(legacyFile)) {
|
|
97
|
+
let targetFileName = file;
|
|
98
|
+
if (file === 'twinclaw.db')
|
|
99
|
+
targetFileName = 'twinbot.db';
|
|
100
|
+
const targetFile = path.join(workspaceDir, targetFileName);
|
|
101
|
+
const targetFileDir = path.dirname(targetFile);
|
|
102
|
+
if (!fs.existsSync(targetFileDir)) {
|
|
103
|
+
fs.mkdirSync(targetFileDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
if (fs.statSync(legacyFile).isDirectory()) {
|
|
106
|
+
fs.cpSync(legacyFile, targetFile, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
fs.copyFileSync(legacyFile, targetFile);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { migrated: true, sourcePath: legacyPath, targetPath: workspacePath };
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
return { migrated: false, sourcePath: legacyPath, targetPath: workspacePath, error: message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export const WORKSPACE_GITIGNORE = `# TwinBot Workspace .gitignore
|
|
121
|
+
# Auto-generated by TwinBot workspace initialization
|
|
122
|
+
|
|
123
|
+
# Sensitive databases
|
|
124
|
+
*.sqlite
|
|
125
|
+
*.sqlite-journal
|
|
126
|
+
*.sqlite-wal
|
|
127
|
+
*.sqlite-shm
|
|
128
|
+
|
|
129
|
+
# Secrets vault (never commit)
|
|
130
|
+
secrets.sqlite
|
|
131
|
+
|
|
132
|
+
# Memory databases
|
|
133
|
+
memory/*.db
|
|
134
|
+
memory/*.db-journal
|
|
135
|
+
|
|
136
|
+
# Session transcripts (may contain sensitive data)
|
|
137
|
+
transcripts/*.md
|
|
138
|
+
|
|
139
|
+
# Runtime state files
|
|
140
|
+
runtime-state.json
|
|
141
|
+
*.pid
|
|
142
|
+
|
|
143
|
+
# Logs
|
|
144
|
+
logs/
|
|
145
|
+
*.log
|
|
146
|
+
|
|
147
|
+
# Temporary files
|
|
148
|
+
*.tmp
|
|
149
|
+
*.temp
|
|
150
|
+
.cache/
|
|
151
|
+
|
|
152
|
+
# OS files
|
|
153
|
+
.DS_Store
|
|
154
|
+
Thumbs.db
|
|
155
|
+
`;
|
|
156
|
+
export function initializeWorkspaceGitignore() {
|
|
157
|
+
const workspaceDir = getWorkspaceDir();
|
|
158
|
+
const gitignorePath = path.join(workspaceDir, '.gitignore');
|
|
159
|
+
if (fs.existsSync(gitignorePath)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
fs.writeFileSync(gitignorePath, WORKSPACE_GITIGNORE, { encoding: 'utf-8' });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function initializeWorkspace() {
|
|
171
|
+
ensureWorkspaceSubdirs();
|
|
172
|
+
initializeWorkspaceGitignore();
|
|
173
|
+
}
|
|
174
|
+
export function getWorkspaceSummary() {
|
|
175
|
+
const workspaceDir = getWorkspaceDir();
|
|
176
|
+
return {
|
|
177
|
+
workspaceDir,
|
|
178
|
+
configPath: getConfigPath(),
|
|
179
|
+
databasePath: getDatabasePath(),
|
|
180
|
+
identityDir: getIdentityDir(),
|
|
181
|
+
secretsVaultPath: getSecretsVaultPath(),
|
|
182
|
+
transcriptsDir: getTranscriptsDir(),
|
|
183
|
+
profileName: getProfileName(),
|
|
184
|
+
exists: fs.existsSync(workspaceDir),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import WAWebJS from 'whatsapp-web.js';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
const { Client, LocalAuth } = WAWebJS;
|
|
4
|
+
function printUsage() {
|
|
5
|
+
console.log(`Channel commands:
|
|
6
|
+
channels login whatsapp Start interactive login for WhatsApp (QR Code)`);
|
|
7
|
+
}
|
|
8
|
+
async function runWhatsappLogin() {
|
|
9
|
+
console.log('[TwinBot Channels] Starting WhatsApp login sequence...');
|
|
10
|
+
console.log('Initializing secure browser environment...');
|
|
11
|
+
const client = new Client({
|
|
12
|
+
authStrategy: new LocalAuth({ dataPath: './memory/whatsapp_auth' }),
|
|
13
|
+
puppeteer: {
|
|
14
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
client.on('qr', (qr) => {
|
|
18
|
+
console.log('\n[TwinBot Channels] ══════════════════════════════════════════════════');
|
|
19
|
+
console.log('[TwinBot Channels] ACTION REQUIRED: SCAN TO LINK WHATSAPP');
|
|
20
|
+
console.log('[TwinBot Channels] 1. Open WhatsApp on your primary phone');
|
|
21
|
+
console.log('[TwinBot Channels] 2. Tap Menu (⋮) or Settings -> Linked Devices');
|
|
22
|
+
console.log('[TwinBot Channels] 3. Tap "Link a Device" and scan the code below:');
|
|
23
|
+
qrcode.generate(qr, { small: true });
|
|
24
|
+
console.log('[TwinBot Channels] ══════════════════════════════════════════════════\n');
|
|
25
|
+
console.log('Awaiting scan...');
|
|
26
|
+
});
|
|
27
|
+
client.on('ready', () => {
|
|
28
|
+
console.log('\n[TwinBot Channels] ✓ Login Successful!');
|
|
29
|
+
console.log('[TwinBot Channels] WhatsApp session is now explicitly linked.');
|
|
30
|
+
console.log('[TwinBot Channels] You may now close this setup screen.');
|
|
31
|
+
console.log('\nNext Steps:');
|
|
32
|
+
console.log('1. Run `node src/index.ts doctor` to verify channel readiness.');
|
|
33
|
+
console.log('2. Ensure your number is authorized per DM Pairing Policy.');
|
|
34
|
+
// Clean exit
|
|
35
|
+
void client.destroy().then(() => {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
client.on('authenticated', () => {
|
|
40
|
+
console.log('[TwinBot Channels] Authentication state captured. Finalizing session mapping...');
|
|
41
|
+
});
|
|
42
|
+
client.on('auth_failure', (msg) => {
|
|
43
|
+
console.error(`\n[TwinBot Channels] ✗ Authentication Failed: ${msg}`);
|
|
44
|
+
console.error('[TwinBot Channels] Remediation: Try deleting the ./memory/whatsapp_auth directory and run `channels login whatsapp` again.');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
void client.destroy().then(() => {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await client.initialize();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
console.error(`\n[TwinBot Channels] ✗ Client initialization failed: ${message}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Handle \`channels\` CLI commands.
|
|
61
|
+
*/
|
|
62
|
+
export async function handleChannelsCli(argv) {
|
|
63
|
+
const topCommand = argv[0];
|
|
64
|
+
if (topCommand !== 'channels') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const subcommand = argv[1];
|
|
68
|
+
const target = argv[2];
|
|
69
|
+
if (subcommand === 'login' && target === 'whatsapp') {
|
|
70
|
+
// Fire and forget because runWhatsappLogin manages process.exit internally
|
|
71
|
+
void runWhatsappLogin();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
printUsage();
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
package/dist/core/cli.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { runDoctorChecks, formatDoctorReport } from './doctor.js';
|
|
2
|
+
// ── Help text ────────────────────────────────────────────────────────────────
|
|
3
|
+
const HELP_TEXT = `
|
|
4
|
+
Usage: twinbot [command] [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
start Start the TwinBot gateway runtime
|
|
8
|
+
doctor Run diagnostics and validate prerequisites
|
|
9
|
+
onboard Run the interactive onboarding wizard (twinbot.json)
|
|
10
|
+
install One-command Windows bootstrap + optional onboarding handoff
|
|
11
|
+
setup Run the guided configuration wizard
|
|
12
|
+
pairing Manage DM pairing approvals (list/approve)
|
|
13
|
+
secret <subcommand> Manage secrets in the secure vault
|
|
14
|
+
channels <subcmd> Manage messaging channels (e.g. login)
|
|
15
|
+
gateway <subcmd> Manage the TwinBot background service daemon
|
|
16
|
+
logs [--follow] Stream daily structured memory logs from the daemon
|
|
17
|
+
--onboard Run the interactive AI persona-building session
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--help, -h Show this help message
|
|
21
|
+
--json Output in machine-readable JSON format (doctor only)
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
twinbot start
|
|
25
|
+
twinbot doctor
|
|
26
|
+
twinbot doctor --json
|
|
27
|
+
twinbot onboard
|
|
28
|
+
twinbot setup
|
|
29
|
+
twinbot pairing list telegram
|
|
30
|
+
twinbot pairing approve telegram ABCD1234
|
|
31
|
+
twinbot secret list
|
|
32
|
+
twinbot secret set API_SECRET mysecret
|
|
33
|
+
twinbot secret rotate API_SECRET newsecret
|
|
34
|
+
twinbot secret revoke API_SECRET
|
|
35
|
+
twinbot secret doctor
|
|
36
|
+
twinbot gateway install
|
|
37
|
+
twinbot gateway status
|
|
38
|
+
twinbot gateway status --deep
|
|
39
|
+
twinbot gateway restart
|
|
40
|
+
twinbot gateway tailscale
|
|
41
|
+
twinbot logs --follow
|
|
42
|
+
`.trim();
|
|
43
|
+
// ── Command handlers ─────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Handle the `doctor` command.
|
|
46
|
+
* Runs all diagnostic checks and emits a report.
|
|
47
|
+
* Returns `true` when the command was recognized and handled.
|
|
48
|
+
*/
|
|
49
|
+
export function handleDoctorCli(argv) {
|
|
50
|
+
if (argv[0] !== 'doctor')
|
|
51
|
+
return false;
|
|
52
|
+
const asJson = argv.includes('--json');
|
|
53
|
+
try {
|
|
54
|
+
const report = runDoctorChecks();
|
|
55
|
+
const output = formatDoctorReport(report, asJson);
|
|
56
|
+
console.log(output);
|
|
57
|
+
if (report.status === 'critical') {
|
|
58
|
+
process.exitCode = 2;
|
|
59
|
+
}
|
|
60
|
+
else if (report.status === 'degraded') {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
process.exitCode = 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
console.error(`[TwinBot] Doctor check failed: ${message}`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Handle `--help` or `-h` flags.
|
|
76
|
+
|
|
77
|
+
* Returns `true` when the flag was found.
|
|
78
|
+
*/
|
|
79
|
+
export function handleHelpCli(argv) {
|
|
80
|
+
if (!argv.includes('--help') && !argv.includes('-h'))
|
|
81
|
+
return false;
|
|
82
|
+
console.log(HELP_TEXT);
|
|
83
|
+
process.exitCode = 0;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Guard against unknown or mistyped top-level commands.
|
|
88
|
+
* Known commands are allowed to fall through to their own handlers.
|
|
89
|
+
* Returns `true` and sets a non-zero exit code when an unknown command is detected.
|
|
90
|
+
*/
|
|
91
|
+
export function handleUnknownCommand(argv) {
|
|
92
|
+
if (argv.length === 0)
|
|
93
|
+
return false;
|
|
94
|
+
const command = argv[0];
|
|
95
|
+
// Flags and known commands pass through
|
|
96
|
+
const KNOWN_COMMANDS = new Set([
|
|
97
|
+
'pairing',
|
|
98
|
+
'secret',
|
|
99
|
+
'doctor',
|
|
100
|
+
'start',
|
|
101
|
+
'onboard',
|
|
102
|
+
'install',
|
|
103
|
+
'setup',
|
|
104
|
+
'channels',
|
|
105
|
+
'gateway',
|
|
106
|
+
'logs',
|
|
107
|
+
'--onboard',
|
|
108
|
+
'--help',
|
|
109
|
+
'-h',
|
|
110
|
+
'--json',
|
|
111
|
+
]);
|
|
112
|
+
if (KNOWN_COMMANDS.has(command) || command.startsWith('--')) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
console.error(`[TwinBot] Unknown command: '${command}'`);
|
|
116
|
+
console.error(`Run 'twinbot --help' to see available commands.`);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getIdentityDir } from '../config/workspace.js';
|
|
4
|
+
export async function assembleContext(additionalRuntimeContext = '') {
|
|
5
|
+
let soul = '';
|
|
6
|
+
let identity = '';
|
|
7
|
+
let user = '';
|
|
8
|
+
const identityDir = getIdentityDir();
|
|
9
|
+
const readOptionalFile = async (fileName) => {
|
|
10
|
+
try {
|
|
11
|
+
const filePath = path.join(identityDir, fileName);
|
|
12
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
soul = await readOptionalFile('soul.md');
|
|
19
|
+
identity = await readOptionalFile('identity.md');
|
|
20
|
+
user = await readOptionalFile('user.md');
|
|
21
|
+
const compiled = `
|
|
22
|
+
You are TwinBot. Follow your core directives exactly.
|
|
23
|
+
|
|
24
|
+
${soul ? `### CORE SOUL & DIRECTIVES\n${soul}` : ''}
|
|
25
|
+
|
|
26
|
+
${identity ? `### IDENTITY & PERSONA\n${identity}` : ''}
|
|
27
|
+
|
|
28
|
+
${user ? `### USER PREFERENCES\n${user}` : ''}
|
|
29
|
+
|
|
30
|
+
${additionalRuntimeContext ? `### ADDITIONAL CONTEXT (RAG MEMORY)\n${additionalRuntimeContext}` : ''}
|
|
31
|
+
`.trim();
|
|
32
|
+
return compiled;
|
|
33
|
+
}
|