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,320 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { runSetupWizard, OnboardingCancelledError, } from './onboarding.js';
|
|
7
|
+
function parseInstallArgs(args) {
|
|
8
|
+
const parsed = {
|
|
9
|
+
help: false,
|
|
10
|
+
nonInteractive: false,
|
|
11
|
+
skipOnboard: false,
|
|
12
|
+
asJson: false,
|
|
13
|
+
};
|
|
14
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
15
|
+
const token = args[i];
|
|
16
|
+
switch (token) {
|
|
17
|
+
case '--help':
|
|
18
|
+
case '-h':
|
|
19
|
+
parsed.help = true;
|
|
20
|
+
break;
|
|
21
|
+
case '--non-interactive':
|
|
22
|
+
parsed.nonInteractive = true;
|
|
23
|
+
break;
|
|
24
|
+
case '--skip-onboard':
|
|
25
|
+
parsed.skipOnboard = true;
|
|
26
|
+
break;
|
|
27
|
+
case '--json':
|
|
28
|
+
parsed.asJson = true;
|
|
29
|
+
break;
|
|
30
|
+
case '--config': {
|
|
31
|
+
const nextValue = args[i + 1];
|
|
32
|
+
if (!nextValue || nextValue.startsWith('--')) {
|
|
33
|
+
parsed.error = 'Missing value for --config.';
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
parsed.configPathOverride = nextValue;
|
|
37
|
+
i += 1;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
parsed.error = `Unknown option '${token}'.`;
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
function printInstallUsage() {
|
|
48
|
+
console.log(`Install command usage:
|
|
49
|
+
install Run Windows install workflow and optional onboarding handoff
|
|
50
|
+
install --non-interactive Run install without interactive prompts
|
|
51
|
+
install --skip-onboard Skip onboarding handoff
|
|
52
|
+
install --config <path> Override twinbot.json path when onboarding runs
|
|
53
|
+
install --json Emit machine-readable install report
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
function statusIcon(status) {
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 'passed':
|
|
59
|
+
return '✅';
|
|
60
|
+
case 'failed':
|
|
61
|
+
return '❌';
|
|
62
|
+
case 'warning':
|
|
63
|
+
return '⚠️';
|
|
64
|
+
default:
|
|
65
|
+
return '⏭️';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function promptYesNo(question, defaultYes) {
|
|
69
|
+
const rl = readline.createInterface({
|
|
70
|
+
input: process.stdin,
|
|
71
|
+
output: process.stdout,
|
|
72
|
+
});
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
rl.on('SIGINT', () => {
|
|
75
|
+
rl.close();
|
|
76
|
+
reject(new OnboardingCancelledError('Installer confirmation cancelled by user.'));
|
|
77
|
+
});
|
|
78
|
+
rl.question(question, (answer) => {
|
|
79
|
+
rl.close();
|
|
80
|
+
const normalized = answer.trim().toLowerCase();
|
|
81
|
+
if (normalized.length === 0) {
|
|
82
|
+
resolve(defaultYes);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function defaultCommandRunner(command, cwd) {
|
|
90
|
+
const execAsync = promisify(exec);
|
|
91
|
+
const started = Date.now();
|
|
92
|
+
try {
|
|
93
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
94
|
+
cwd,
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
97
|
+
});
|
|
98
|
+
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
99
|
+
return { ok: true, exitCode: 0, output, durationMs: Date.now() - started };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const err = error;
|
|
103
|
+
const output = [err.stdout, err.stderr, err.message]
|
|
104
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
105
|
+
.join('\n')
|
|
106
|
+
.trim();
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
exitCode: typeof err.code === 'number' ? err.code : 1,
|
|
110
|
+
output,
|
|
111
|
+
durationMs: Date.now() - started,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function runInstallFlow(options) {
|
|
116
|
+
const logger = options.logger ?? console;
|
|
117
|
+
const cwd = options.cwd ?? process.cwd();
|
|
118
|
+
const commandRunner = options.commandRunner ?? defaultCommandRunner;
|
|
119
|
+
const onboardingRunner = options.onboardingRunner ?? runSetupWizard;
|
|
120
|
+
const confirmOnboard = options.confirmOnboard ??
|
|
121
|
+
(() => promptYesNo('\nLaunch onboarding wizard now? [Y/n]: ', true));
|
|
122
|
+
const steps = [];
|
|
123
|
+
const warnings = [];
|
|
124
|
+
const errors = [];
|
|
125
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
126
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
|
|
127
|
+
if (!existsSync(packagePath)) {
|
|
128
|
+
errors.push(`package.json is missing from ${cwd}.`);
|
|
129
|
+
steps.push({
|
|
130
|
+
id: 'preflight',
|
|
131
|
+
status: 'failed',
|
|
132
|
+
detail: 'Missing package.json in current working directory.',
|
|
133
|
+
});
|
|
134
|
+
return { status: 'failed', steps, warnings, errors };
|
|
135
|
+
}
|
|
136
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < 22) {
|
|
137
|
+
errors.push(`Node.js v22+ required. Detected v${process.versions.node}.`);
|
|
138
|
+
steps.push({
|
|
139
|
+
id: 'preflight',
|
|
140
|
+
status: 'failed',
|
|
141
|
+
detail: `Node.js v22+ required. Detected v${process.versions.node}.`,
|
|
142
|
+
});
|
|
143
|
+
return { status: 'failed', steps, warnings, errors };
|
|
144
|
+
}
|
|
145
|
+
steps.push({
|
|
146
|
+
id: 'preflight',
|
|
147
|
+
status: 'passed',
|
|
148
|
+
detail: `Preflight checks passed (Node.js v${process.versions.node}, package.json present).`,
|
|
149
|
+
});
|
|
150
|
+
const dependenciesCommand = 'npm install --no-audit --no-fund';
|
|
151
|
+
const installRun = await commandRunner(dependenciesCommand, cwd);
|
|
152
|
+
if (!installRun.ok) {
|
|
153
|
+
const detail = `Dependency install failed (exit ${installRun.exitCode}).`;
|
|
154
|
+
errors.push(`${detail} ${installRun.output.trim().split('\n').slice(-5).join(' ')}`.trim());
|
|
155
|
+
steps.push({
|
|
156
|
+
id: 'dependencies',
|
|
157
|
+
status: 'failed',
|
|
158
|
+
detail,
|
|
159
|
+
command: dependenciesCommand,
|
|
160
|
+
});
|
|
161
|
+
return { status: 'failed', steps, warnings, errors };
|
|
162
|
+
}
|
|
163
|
+
steps.push({
|
|
164
|
+
id: 'dependencies',
|
|
165
|
+
status: 'passed',
|
|
166
|
+
detail: 'Dependencies installed successfully.',
|
|
167
|
+
command: dependenciesCommand,
|
|
168
|
+
});
|
|
169
|
+
const hooksCommand = 'npm run setup:hooks';
|
|
170
|
+
if (existsSync(path.join(cwd, '.git'))) {
|
|
171
|
+
const hooksRun = await commandRunner(hooksCommand, cwd);
|
|
172
|
+
if (!hooksRun.ok) {
|
|
173
|
+
warnings.push('Unable to configure .githooks path automatically.');
|
|
174
|
+
steps.push({
|
|
175
|
+
id: 'git-hooks',
|
|
176
|
+
status: 'warning',
|
|
177
|
+
detail: 'Hook setup failed. Continue manually with `npm run setup:hooks`.',
|
|
178
|
+
command: hooksCommand,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
steps.push({
|
|
183
|
+
id: 'git-hooks',
|
|
184
|
+
status: 'passed',
|
|
185
|
+
detail: 'Git hook path configured.',
|
|
186
|
+
command: hooksCommand,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
steps.push({
|
|
192
|
+
id: 'git-hooks',
|
|
193
|
+
status: 'skipped',
|
|
194
|
+
detail: 'No .git directory found; hook setup skipped.',
|
|
195
|
+
command: hooksCommand,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (options.skipOnboard) {
|
|
199
|
+
steps.push({
|
|
200
|
+
id: 'onboarding',
|
|
201
|
+
status: 'skipped',
|
|
202
|
+
detail: 'Onboarding handoff skipped by --skip-onboard.',
|
|
203
|
+
});
|
|
204
|
+
return { status: 'success', steps, warnings, errors };
|
|
205
|
+
}
|
|
206
|
+
if (options.nonInteractive) {
|
|
207
|
+
warnings.push('Non-interactive install mode skips onboarding. Run `twinbot onboard` next.');
|
|
208
|
+
steps.push({
|
|
209
|
+
id: 'onboarding',
|
|
210
|
+
status: 'skipped',
|
|
211
|
+
detail: 'Onboarding skipped in non-interactive mode.',
|
|
212
|
+
});
|
|
213
|
+
return { status: 'success', steps, warnings, errors };
|
|
214
|
+
}
|
|
215
|
+
const shouldRunOnboard = await confirmOnboard();
|
|
216
|
+
if (!shouldRunOnboard) {
|
|
217
|
+
steps.push({
|
|
218
|
+
id: 'onboarding',
|
|
219
|
+
status: 'skipped',
|
|
220
|
+
detail: 'Onboarding deferred by operator.',
|
|
221
|
+
});
|
|
222
|
+
return { status: 'success', steps, warnings, errors };
|
|
223
|
+
}
|
|
224
|
+
const onboardingResult = await onboardingRunner({
|
|
225
|
+
configPathOverride: options.configPathOverride,
|
|
226
|
+
});
|
|
227
|
+
if (onboardingResult.status === 'cancelled') {
|
|
228
|
+
warnings.push('Onboarding cancelled; install completed without persisted onboarding updates.');
|
|
229
|
+
steps.push({
|
|
230
|
+
id: 'onboarding',
|
|
231
|
+
status: 'warning',
|
|
232
|
+
detail: 'Onboarding was cancelled by user.',
|
|
233
|
+
});
|
|
234
|
+
return { status: 'cancelled', steps, warnings, errors };
|
|
235
|
+
}
|
|
236
|
+
if (onboardingResult.status !== 'success') {
|
|
237
|
+
const detail = `Onboarding failed with status '${onboardingResult.status}'.`;
|
|
238
|
+
errors.push(...onboardingResult.errors);
|
|
239
|
+
steps.push({
|
|
240
|
+
id: 'onboarding',
|
|
241
|
+
status: 'failed',
|
|
242
|
+
detail,
|
|
243
|
+
});
|
|
244
|
+
return { status: 'failed', steps, warnings, errors };
|
|
245
|
+
}
|
|
246
|
+
warnings.push(...onboardingResult.warnings);
|
|
247
|
+
steps.push({
|
|
248
|
+
id: 'onboarding',
|
|
249
|
+
status: 'passed',
|
|
250
|
+
detail: `Onboarding completed. Config saved to ${onboardingResult.configPath}.`,
|
|
251
|
+
});
|
|
252
|
+
return { status: 'success', steps, warnings, errors };
|
|
253
|
+
}
|
|
254
|
+
function printInstallReport(report, logger) {
|
|
255
|
+
logger.log('\nTwinBot Install Report');
|
|
256
|
+
logger.log('──────────────────────────────────────────────────');
|
|
257
|
+
for (const step of report.steps) {
|
|
258
|
+
logger.log(`${statusIcon(step.status)} [${step.id}] ${step.detail}`);
|
|
259
|
+
}
|
|
260
|
+
if (report.warnings.length > 0) {
|
|
261
|
+
logger.warn('\nWarnings:');
|
|
262
|
+
for (const warning of report.warnings) {
|
|
263
|
+
logger.warn(`- ${warning}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (report.errors.length > 0) {
|
|
267
|
+
logger.error('\nErrors:');
|
|
268
|
+
for (const error of report.errors) {
|
|
269
|
+
logger.error(`- ${error}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Handles one-shot `install` command.
|
|
275
|
+
*/
|
|
276
|
+
export async function handleInstallCli(argv) {
|
|
277
|
+
if (argv[0] !== 'install') {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const parsed = parseInstallArgs(argv.slice(1));
|
|
281
|
+
if (parsed.help) {
|
|
282
|
+
printInstallUsage();
|
|
283
|
+
process.exitCode = 0;
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (parsed.error) {
|
|
287
|
+
console.error(`[TwinBot Install] ${parsed.error}`);
|
|
288
|
+
printInstallUsage();
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const result = await runInstallFlow({
|
|
294
|
+
nonInteractive: parsed.nonInteractive,
|
|
295
|
+
skipOnboard: parsed.skipOnboard,
|
|
296
|
+
configPathOverride: parsed.configPathOverride,
|
|
297
|
+
});
|
|
298
|
+
if (parsed.asJson) {
|
|
299
|
+
console.log(JSON.stringify(result, null, 2));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
printInstallReport(result, console);
|
|
303
|
+
}
|
|
304
|
+
if (result.status === 'success') {
|
|
305
|
+
process.exitCode = 0;
|
|
306
|
+
}
|
|
307
|
+
else if (result.status === 'cancelled') {
|
|
308
|
+
process.exitCode = 130;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
process.exitCode = 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
316
|
+
console.error(`[TwinBot Install] ${message}`);
|
|
317
|
+
process.exitCode = error instanceof OnboardingCancelledError ? 130 : 1;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { logToolCall, scrubSensitiveText } from '../utils/logger.js';
|
|
2
|
+
/** Convert a Skill (from the registry) into the internal Tool format used by LaneExecutor. */
|
|
3
|
+
function skillToTool(skill) {
|
|
4
|
+
return {
|
|
5
|
+
name: skill.name,
|
|
6
|
+
description: skill.description,
|
|
7
|
+
parameters: skill.parameters ?? {},
|
|
8
|
+
group: skill.group,
|
|
9
|
+
aliases: skill.aliases,
|
|
10
|
+
mcpScope: skill.mcpScope,
|
|
11
|
+
serverId: skill.serverId,
|
|
12
|
+
adapter: skill.adapter,
|
|
13
|
+
execute: async (args) => {
|
|
14
|
+
const result = await skill.execute(args);
|
|
15
|
+
return result.output;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export class LaneExecutor {
|
|
20
|
+
tools = new Map();
|
|
21
|
+
constructor(tools = []) {
|
|
22
|
+
for (const tool of tools) {
|
|
23
|
+
this.tools.set(tool.name, tool);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
registerTool(tool) {
|
|
27
|
+
this.tools.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
registerSkill(skill) {
|
|
30
|
+
const converted = skillToTool(skill);
|
|
31
|
+
this.tools.set(converted.name, converted);
|
|
32
|
+
for (const alias of skill.aliases ?? []) {
|
|
33
|
+
const normalized = alias.trim();
|
|
34
|
+
if (!normalized || normalized === converted.name) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
this.tools.set(normalized, converted);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
syncSkills(skills) {
|
|
41
|
+
this.tools.clear();
|
|
42
|
+
for (const skill of skills) {
|
|
43
|
+
this.registerSkill(skill);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pull all skills from a SkillRegistry and replace the executable tool map.
|
|
48
|
+
*/
|
|
49
|
+
syncFromRegistry(registry) {
|
|
50
|
+
this.syncSkills(registry.list());
|
|
51
|
+
}
|
|
52
|
+
parseArguments(args) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(args);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.warn(`[LaneExecutor] Failed to parse arguments: ${scrubSensitiveText(args)}`);
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async executeToolCalls(message, sessionId, policyEngine) {
|
|
62
|
+
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const results = [];
|
|
66
|
+
// Lane-Based Execution: Execute tools serially in an await loop
|
|
67
|
+
for (const toolCall of message.tool_calls) {
|
|
68
|
+
const toolName = toolCall.function.name;
|
|
69
|
+
const args = this.parseArguments(toolCall.function.arguments);
|
|
70
|
+
const tool = this.tools.get(toolName);
|
|
71
|
+
let content = '';
|
|
72
|
+
if (!tool) {
|
|
73
|
+
console.warn(`[LaneExecutor] Tool not found: ${toolName}`);
|
|
74
|
+
content = `Error: Tool '${toolName}' is not registered or unavailable.`;
|
|
75
|
+
await logToolCall(toolName, args, content);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
let allowed = true;
|
|
79
|
+
let decision = policyEngine ? policyEngine.evaluate(sessionId, toolName) : null;
|
|
80
|
+
// 1. MCP Scope Enforcement (Applies before or alongside PolicyEngine)
|
|
81
|
+
if (tool.serverId && tool.mcpScope) {
|
|
82
|
+
if (tool.mcpScope === 'unclassified') {
|
|
83
|
+
content = `Access Denied: MCP tool '${toolName}' is unclassified (secure default). Capability Profile: ${tool.mcpScope}`;
|
|
84
|
+
allowed = false;
|
|
85
|
+
tool.adapter?.auditScopeBlock(sessionId, toolName, tool.mcpScope, 'Secure default for unclassified tools');
|
|
86
|
+
}
|
|
87
|
+
else if (tool.mcpScope === 'high-risk') {
|
|
88
|
+
// High-risk blocked-by-default: Needs explicit policy allow, not a fallback
|
|
89
|
+
const isFallback = decision ? decision.reason.includes('Fell back to') : true;
|
|
90
|
+
if (isFallback) {
|
|
91
|
+
content = `Access Denied: MCP tool '${toolName}' is 'high-risk' and blocked by default. Requires explicit allow rule. Capability Profile: ${tool.mcpScope}`;
|
|
92
|
+
allowed = false;
|
|
93
|
+
tool.adapter?.auditScopeBlock(sessionId, toolName, tool.mcpScope, 'High-risk tools require explicit allow rule');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (allowed) {
|
|
97
|
+
tool.adapter?.auditScopeAllow(sessionId, toolName, tool.mcpScope);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 2. Policy Governance Baseline
|
|
101
|
+
if (allowed && decision && decision.action === 'deny') {
|
|
102
|
+
content = `Access Denied: Tool '${toolName}' is blocked by policy. Reason: ${decision.reason}`;
|
|
103
|
+
allowed = false;
|
|
104
|
+
}
|
|
105
|
+
if (!allowed) {
|
|
106
|
+
console.warn(`[LaneExecutor] Blocked tool ${toolName}: ${content}`);
|
|
107
|
+
await logToolCall(toolName, args, content);
|
|
108
|
+
}
|
|
109
|
+
if (allowed) {
|
|
110
|
+
try {
|
|
111
|
+
console.log(`[LaneExecutor] Executing ${toolName} with args: ${scrubSensitiveText(JSON.stringify(args))}`);
|
|
112
|
+
const result = await tool.execute(args);
|
|
113
|
+
content = typeof result === 'string' ? result : JSON.stringify(result);
|
|
114
|
+
await logToolCall(toolName, args, content);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
118
|
+
const sanitizedMessage = scrubSensitiveText(rawMessage);
|
|
119
|
+
console.error(`[LaneExecutor] Tool ${toolName} failed: ${sanitizedMessage}`);
|
|
120
|
+
content = `Error executing tool: ${sanitizedMessage}`;
|
|
121
|
+
await logToolCall(toolName, args, content);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
results.push({
|
|
126
|
+
role: 'tool',
|
|
127
|
+
tool_call_id: toolCall.id,
|
|
128
|
+
name: toolName,
|
|
129
|
+
content: content,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as fsPromises from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function currentDateIso() {
|
|
5
|
+
return new Date().toISOString().slice(0, 10);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Handle the `logs` command.
|
|
9
|
+
* Reads or tails the daily TwinBot memory log file.
|
|
10
|
+
*/
|
|
11
|
+
export async function handleLogsCli(argv) {
|
|
12
|
+
if (argv[0] !== 'logs')
|
|
13
|
+
return false;
|
|
14
|
+
const follow = argv.includes('--follow') || argv.includes('-f');
|
|
15
|
+
const dateIso = currentDateIso();
|
|
16
|
+
const logPath = path.resolve('memory', `${dateIso}.md`);
|
|
17
|
+
if (!fs.existsSync(logPath)) {
|
|
18
|
+
console.error(`[TwinBot Logs] No logs found for today (${dateIso}) at ${logPath}.`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (follow) {
|
|
23
|
+
console.log(`[TwinBot Logs] Following logs from ${logPath}...\n`);
|
|
24
|
+
tailFile(logPath);
|
|
25
|
+
// We do not exit the process here to keep the watcher alive.
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const contents = await fsPromises.readFile(logPath, 'utf8');
|
|
29
|
+
process.stdout.write(contents);
|
|
30
|
+
process.exitCode = 0;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Tail a file similar to `tail -f`.
|
|
36
|
+
*/
|
|
37
|
+
function tailFile(filePath) {
|
|
38
|
+
let position = fs.statSync(filePath).size;
|
|
39
|
+
// For tailing, print the last 4KB of context first context
|
|
40
|
+
const startPos = Math.max(0, position - 4096);
|
|
41
|
+
if (startPos < position) {
|
|
42
|
+
const initialStream = fs.createReadStream(filePath, { start: startPos, encoding: 'utf8' });
|
|
43
|
+
initialStream.pipe(process.stdout);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
fs.watch(filePath, (eventType) => {
|
|
47
|
+
if (eventType === 'change') {
|
|
48
|
+
const stats = fs.statSync(filePath);
|
|
49
|
+
if (stats.size > position) {
|
|
50
|
+
const stream = fs.createReadStream(filePath, {
|
|
51
|
+
start: position,
|
|
52
|
+
end: stats.size,
|
|
53
|
+
encoding: 'utf8'
|
|
54
|
+
});
|
|
55
|
+
stream.on('data', (chunk) => {
|
|
56
|
+
process.stdout.write(chunk);
|
|
57
|
+
});
|
|
58
|
+
position = stats.size;
|
|
59
|
+
}
|
|
60
|
+
else if (stats.size < position) {
|
|
61
|
+
// File was truncated or rolled over
|
|
62
|
+
position = stats.size;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(`[TwinBot Logs] Failed to watch file: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|