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.
Files changed (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. 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
+ }