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,92 @@
1
+ {
2
+ "packages": [
3
+ {
4
+ "name": "filesystem",
5
+ "version": "1.0.0",
6
+ "metadata": {
7
+ "displayName": "Filesystem MCP Skill Pack",
8
+ "description": "Local filesystem operations through MCP."
9
+ },
10
+ "dependencies": {},
11
+ "compatibility": {
12
+ "node": ">=22.0.0",
13
+ "runtimeApi": "^1.0.0",
14
+ "requiredTools": [
15
+ "npx"
16
+ ]
17
+ },
18
+ "server": {
19
+ "id": "filesystem",
20
+ "name": "Filesystem",
21
+ "description": "Read, write, and manage local files and directories securely.",
22
+ "transport": "stdio",
23
+ "command": "npx",
24
+ "args": [
25
+ "-y",
26
+ "@modelcontextprotocol/server-filesystem",
27
+ "."
28
+ ],
29
+ "autoConnect": true,
30
+ "enabled": true
31
+ }
32
+ },
33
+ {
34
+ "name": "fetch",
35
+ "version": "1.0.0",
36
+ "metadata": {
37
+ "displayName": "Fetch MCP Skill Pack",
38
+ "description": "HTTP request and web resource retrieval skills."
39
+ },
40
+ "dependencies": {},
41
+ "compatibility": {
42
+ "node": ">=22.0.0",
43
+ "runtimeApi": "^1.0.0",
44
+ "requiredTools": [
45
+ "npx"
46
+ ]
47
+ },
48
+ "server": {
49
+ "id": "fetch",
50
+ "name": "Fetch (HTTP)",
51
+ "description": "Make HTTP requests to external APIs and web resources with content extraction.",
52
+ "transport": "stdio",
53
+ "command": "npx",
54
+ "args": [
55
+ "-y",
56
+ "@modelcontextprotocol/server-fetch"
57
+ ],
58
+ "autoConnect": true,
59
+ "enabled": true
60
+ }
61
+ },
62
+ {
63
+ "name": "sequential-thinking",
64
+ "version": "1.0.0",
65
+ "metadata": {
66
+ "displayName": "Sequential Thinking MCP Skill Pack",
67
+ "description": "Structured decomposition and reasoning assistant tools."
68
+ },
69
+ "dependencies": {},
70
+ "compatibility": {
71
+ "node": ">=22.0.0",
72
+ "runtimeApi": "^1.0.0",
73
+ "requiredTools": [
74
+ "npx"
75
+ ]
76
+ },
77
+ "server": {
78
+ "id": "sequential-thinking",
79
+ "name": "Sequential Thinking",
80
+ "description": "Structured, step-by-step reasoning and problem decomposition for complex tasks.",
81
+ "transport": "stdio",
82
+ "command": "npx",
83
+ "args": [
84
+ "-y",
85
+ "@modelcontextprotocol/server-sequential-thinking"
86
+ ],
87
+ "autoConnect": true,
88
+ "enabled": true
89
+ }
90
+ }
91
+ ]
92
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "generatedAt": "1970-01-01T00:00:00.000Z",
4
+ "packages": {}
5
+ }
@@ -0,0 +1,275 @@
1
+ import { appendFile, readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { Skill } from './types.js';
6
+ import { executeShell } from './shell.js';
7
+ import { logToolCall } from '../utils/logger.js';
8
+
9
+ function resolveWorkspacePath(inputPath: string): string {
10
+ return path.resolve(process.cwd(), inputPath);
11
+ }
12
+
13
+ function quoteShellArg(value: string): string {
14
+ return `"${value.replace(/"/g, '\\"')}"`;
15
+ }
16
+
17
+ function buildReadFileSkill(): Skill {
18
+ return {
19
+ name: 'fs.read',
20
+ group: 'group:fs',
21
+ aliases: ['read_file'],
22
+ description: 'Read a UTF-8 text file from disk.',
23
+ parameters: {
24
+ type: 'object',
25
+ properties: {
26
+ filePath: { type: 'string' },
27
+ },
28
+ required: ['filePath'],
29
+ },
30
+ async execute(input) {
31
+ const filePathValue = input.filePath;
32
+ if (typeof filePathValue !== 'string' || filePathValue.trim().length === 0) {
33
+ return { ok: false, output: 'filePath must be a non-empty string.' };
34
+ }
35
+
36
+ const absolutePath = resolveWorkspacePath(filePathValue);
37
+ const content = await readFile(absolutePath, 'utf8');
38
+
39
+ await logToolCall('fs.read', input, `Read ${content.length} chars from ${absolutePath}`);
40
+ return { ok: true, output: content };
41
+ },
42
+ };
43
+ }
44
+
45
+ function buildListFilesSkill(): Skill {
46
+ return {
47
+ name: 'fs.list',
48
+ group: 'group:fs',
49
+ aliases: ['list_files'],
50
+ description: 'List files and folders in a directory.',
51
+ parameters: {
52
+ type: 'object',
53
+ properties: {
54
+ dirPath: { type: 'string' },
55
+ },
56
+ required: [],
57
+ },
58
+ async execute(input) {
59
+ const dirPathValue = typeof input.dirPath === 'string' ? input.dirPath : '.';
60
+ const absolutePath = resolveWorkspacePath(dirPathValue);
61
+
62
+ const entries = await readdir(absolutePath, { withFileTypes: true });
63
+ const output = entries
64
+ .map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`)
65
+ .join('\n');
66
+
67
+ await logToolCall('fs.list', input, `Listed ${entries.length} entries from ${absolutePath}`);
68
+ return { ok: true, output };
69
+ },
70
+ };
71
+ }
72
+
73
+ function buildWriteFileSkill(): Skill {
74
+ return {
75
+ name: 'fs.write',
76
+ group: 'group:fs',
77
+ description: 'Write UTF-8 text content to a file path.',
78
+ parameters: {
79
+ type: 'object',
80
+ properties: {
81
+ filePath: { type: 'string' },
82
+ content: { type: 'string' },
83
+ append: { type: 'boolean' },
84
+ },
85
+ required: ['filePath', 'content'],
86
+ },
87
+ async execute(input) {
88
+ const filePathValue = input.filePath;
89
+ if (typeof filePathValue !== 'string' || filePathValue.trim().length === 0) {
90
+ return { ok: false, output: 'filePath must be a non-empty string.' };
91
+ }
92
+
93
+ const contentValue = input.content;
94
+ if (typeof contentValue !== 'string') {
95
+ return { ok: false, output: 'content must be a string.' };
96
+ }
97
+
98
+ const absolutePath = resolveWorkspacePath(filePathValue);
99
+ if (input.append === true) {
100
+ await appendFile(absolutePath, contentValue, 'utf8');
101
+ } else {
102
+ await writeFile(absolutePath, contentValue, 'utf8');
103
+ }
104
+ await logToolCall('fs.write', input, `Wrote ${contentValue.length} chars to ${absolutePath}`);
105
+ return { ok: true, output: `Updated ${absolutePath}.` };
106
+ },
107
+ };
108
+ }
109
+
110
+ function buildApplyPatchSkill(): Skill {
111
+ return {
112
+ name: 'fs.apply_patch',
113
+ group: 'group:fs',
114
+ aliases: ['apply_patch'],
115
+ description: 'Apply a unified diff patch to files in the current workspace using git apply.',
116
+ parameters: {
117
+ type: 'object',
118
+ properties: {
119
+ patch: { type: 'string' },
120
+ },
121
+ required: ['patch'],
122
+ },
123
+ async execute(input) {
124
+ const patchValue = input.patch;
125
+ if (typeof patchValue !== 'string' || patchValue.trim().length === 0) {
126
+ return { ok: false, output: 'patch must be a non-empty string.' };
127
+ }
128
+
129
+ const tempPatchPath = path.join(tmpdir(), `twinbot-${randomUUID()}.patch`);
130
+ try {
131
+ await writeFile(tempPatchPath, patchValue, 'utf8');
132
+ const result = await executeShell(
133
+ `git apply --whitespace=nowarn ${quoteShellArg(tempPatchPath)}`,
134
+ process.cwd(),
135
+ { timeoutMs: 20_000 },
136
+ );
137
+ if (!result.ok) {
138
+ await logToolCall('fs.apply_patch', input, `Patch apply failed: ${result.output}`);
139
+ return {
140
+ ok: false,
141
+ output: `Failed to apply patch: ${result.output || 'git apply returned a non-zero exit code.'}`,
142
+ };
143
+ }
144
+
145
+ await logToolCall('fs.apply_patch', input, 'Patch applied successfully.');
146
+ return { ok: true, output: 'Patch applied successfully.' };
147
+ } finally {
148
+ await rm(tempPatchPath, { force: true }).catch(() => undefined);
149
+ }
150
+ },
151
+ };
152
+ }
153
+
154
+ function resolveShellOptions(input: Record<string, unknown>): { timeoutMs?: number; allowUnsafe?: boolean } {
155
+ const timeoutMs =
156
+ typeof input.timeoutMs === 'number' && Number.isFinite(input.timeoutMs)
157
+ ? Math.floor(input.timeoutMs)
158
+ : undefined;
159
+ const allowUnsafe = input.allowUnsafe === true;
160
+ return { timeoutMs, allowUnsafe };
161
+ }
162
+
163
+ function buildRuntimeExecSkill(): Skill {
164
+ return {
165
+ name: 'runtime.exec',
166
+ group: 'group:runtime',
167
+ aliases: ['shell_execute'],
168
+ description: 'Execute a shell command with timeout and safety checks.',
169
+ parameters: {
170
+ type: 'object',
171
+ properties: {
172
+ command: { type: 'string' },
173
+ cwd: { type: 'string' },
174
+ timeoutMs: { type: 'number' },
175
+ allowUnsafe: { type: 'boolean' },
176
+ },
177
+ required: ['command'],
178
+ },
179
+ async execute(input) {
180
+ const commandValue = input.command;
181
+ if (typeof commandValue !== 'string' || commandValue.trim().length === 0) {
182
+ return { ok: false, output: 'command must be a non-empty string.' };
183
+ }
184
+ const cwd = typeof input.cwd === 'string' ? resolveWorkspacePath(input.cwd) : undefined;
185
+ const result = await executeShell(commandValue, cwd, resolveShellOptions(input));
186
+ await logToolCall('runtime.exec', input, result.output || '(no output)');
187
+
188
+ return {
189
+ ok: result.ok,
190
+ output: result.output,
191
+ };
192
+ },
193
+ };
194
+ }
195
+
196
+ function buildRuntimePowerShellSkill(): Skill {
197
+ return {
198
+ name: 'runtime.powershell',
199
+ group: 'group:runtime',
200
+ description: 'Execute a script using Windows PowerShell with timeout and safety checks.',
201
+ parameters: {
202
+ type: 'object',
203
+ properties: {
204
+ script: { type: 'string' },
205
+ cwd: { type: 'string' },
206
+ timeoutMs: { type: 'number' },
207
+ allowUnsafe: { type: 'boolean' },
208
+ },
209
+ required: ['script'],
210
+ },
211
+ async execute(input) {
212
+ const scriptValue = input.script;
213
+ if (typeof scriptValue !== 'string' || scriptValue.trim().length === 0) {
214
+ return { ok: false, output: 'script must be a non-empty string.' };
215
+ }
216
+ const cwd = typeof input.cwd === 'string' ? resolveWorkspacePath(input.cwd) : undefined;
217
+ const command = `powershell -NoProfile -ExecutionPolicy Bypass -Command ${quoteShellArg(scriptValue)}`;
218
+ const result = await executeShell(command, cwd, resolveShellOptions(input));
219
+ await logToolCall('runtime.powershell', input, result.output || '(no output)');
220
+ return {
221
+ ok: result.ok,
222
+ output: result.output,
223
+ };
224
+ },
225
+ };
226
+ }
227
+
228
+ function buildRuntimeProcessSkill(): Skill {
229
+ return {
230
+ name: 'runtime.process',
231
+ group: 'group:runtime',
232
+ description: 'Execute an executable with argument array under shell safety constraints.',
233
+ parameters: {
234
+ type: 'object',
235
+ properties: {
236
+ executable: { type: 'string' },
237
+ args: { type: 'array', items: { type: 'string' } },
238
+ cwd: { type: 'string' },
239
+ timeoutMs: { type: 'number' },
240
+ allowUnsafe: { type: 'boolean' },
241
+ },
242
+ required: ['executable'],
243
+ },
244
+ async execute(input) {
245
+ const executableValue = input.executable;
246
+ if (typeof executableValue !== 'string' || executableValue.trim().length === 0) {
247
+ return { ok: false, output: 'executable must be a non-empty string.' };
248
+ }
249
+
250
+ const argsValue = Array.isArray(input.args)
251
+ ? input.args.filter((arg): arg is string => typeof arg === 'string')
252
+ : [];
253
+ const command = [quoteShellArg(executableValue), ...argsValue.map(quoteShellArg)].join(' ');
254
+ const cwd = typeof input.cwd === 'string' ? resolveWorkspacePath(input.cwd) : undefined;
255
+ const result = await executeShell(command, cwd, resolveShellOptions(input));
256
+ await logToolCall('runtime.process', input, result.output || '(no output)');
257
+ return {
258
+ ok: result.ok,
259
+ output: result.output,
260
+ };
261
+ },
262
+ };
263
+ }
264
+
265
+ export function createBuiltinSkills(): Skill[] {
266
+ return [
267
+ buildReadFileSkill(),
268
+ buildListFilesSkill(),
269
+ buildWriteFileSkill(),
270
+ buildApplyPatchSkill(),
271
+ buildRuntimeExecSkill(),
272
+ buildRuntimePowerShellSkill(),
273
+ buildRuntimeProcessSkill(),
274
+ ];
275
+ }
@@ -0,0 +1,118 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { logSystemCommand, scrubSensitiveText } from '../utils/logger.js';
4
+
5
+ const execAsync = promisify(exec);
6
+ const DEFAULT_TIMEOUT_MS = 15_000;
7
+ const MAX_TIMEOUT_MS = 120_000;
8
+ const MAX_OUTPUT_LENGTH = 8_000;
9
+ const BLOCKED_COMMAND_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
10
+ { pattern: /\brm\s+-rf\s+\/\b/i, reason: 'destructive root delete' },
11
+ { pattern: /\brm\s+-rf\s+\*/i, reason: 'destructive wildcard delete' },
12
+ { pattern: /\bdel\s+\/[A-Za-z]*\s+\/[A-Za-z]*\s+[A-Za-z]:\\/i, reason: 'destructive windows delete' },
13
+ { pattern: /\bformat\s+[A-Za-z]:/i, reason: 'disk format command' },
14
+ { pattern: /\bshutdown\b/i, reason: 'system shutdown command' },
15
+ { pattern: /\breboot\b/i, reason: 'system reboot command' },
16
+ { pattern: /\bpoweroff\b/i, reason: 'system poweroff command' },
17
+ ];
18
+
19
+ export interface ShellExecutionOptions {
20
+ timeoutMs?: number;
21
+ allowUnsafe?: boolean;
22
+ }
23
+
24
+ interface ExecError extends Error {
25
+ code?: number | string;
26
+ stdout?: string;
27
+ stderr?: string;
28
+ }
29
+
30
+
31
+ function truncateOutput(output: string): string {
32
+ if (output.length <= MAX_OUTPUT_LENGTH) {
33
+ return output;
34
+ }
35
+
36
+ return `${output.slice(0, MAX_OUTPUT_LENGTH)}\n...[truncated]`;
37
+ }
38
+
39
+ function resolveTimeout(timeoutMs?: number): number {
40
+ if (!Number.isFinite(timeoutMs)) {
41
+ return DEFAULT_TIMEOUT_MS;
42
+ }
43
+
44
+ const parsed = Math.floor(Number(timeoutMs));
45
+ if (parsed < 1) {
46
+ return DEFAULT_TIMEOUT_MS;
47
+ }
48
+ return Math.min(MAX_TIMEOUT_MS, parsed);
49
+ }
50
+
51
+ function detectBlockedCommand(command: string): string | null {
52
+ for (const entry of BLOCKED_COMMAND_PATTERNS) {
53
+ if (entry.pattern.test(command)) {
54
+ return entry.reason;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export async function executeShell(
61
+ command: string,
62
+ cwd?: string,
63
+ options: ShellExecutionOptions = {},
64
+ ): Promise<{ ok: boolean; output: string; exitCode: number }> {
65
+ const normalizedCommand = command.trim();
66
+ if (normalizedCommand.length === 0) {
67
+ return {
68
+ ok: false,
69
+ output: 'Command must be a non-empty string.',
70
+ exitCode: 1,
71
+ };
72
+ }
73
+
74
+ const blockedReason = options.allowUnsafe ? null : detectBlockedCommand(normalizedCommand);
75
+ if (blockedReason) {
76
+ const blockedOutput = `Blocked unsafe command (${blockedReason}). Set allowUnsafe=true to override.`;
77
+ await logSystemCommand(normalizedCommand, blockedOutput, 126);
78
+ return {
79
+ ok: false,
80
+ output: blockedOutput,
81
+ exitCode: 126,
82
+ };
83
+ }
84
+
85
+ const timeout = resolveTimeout(options.timeoutMs);
86
+
87
+ try {
88
+ const { stdout, stderr } = await execAsync(normalizedCommand, {
89
+ cwd,
90
+ timeout,
91
+ windowsHide: true,
92
+ maxBuffer: 1024 * 1024,
93
+ });
94
+
95
+ const mergedOutput = truncateOutput(scrubSensitiveText(`${stdout}${stderr}`.trim()));
96
+ await logSystemCommand(normalizedCommand, mergedOutput || '(no output)', 0);
97
+
98
+ return {
99
+ ok: true,
100
+ output: mergedOutput,
101
+ exitCode: 0,
102
+ };
103
+ } catch (error: unknown) {
104
+ const err = error as ExecError;
105
+ const mergedOutput = truncateOutput(
106
+ scrubSensitiveText(`${err.stdout ?? ''}${err.stderr ?? ''}${err.message ?? ''}`.trim()),
107
+ );
108
+
109
+ const exitCode = typeof err.code === 'number' ? err.code : 1;
110
+ await logSystemCommand(normalizedCommand, mergedOutput || '(no output)', exitCode);
111
+
112
+ return {
113
+ ok: false,
114
+ output: mergedOutput,
115
+ exitCode,
116
+ };
117
+ }
118
+ }
@@ -0,0 +1,30 @@
1
+ import type { JsonSchema, McpCapabilityScope, McpScopeAuditAdapter } from '../types/mcp.js';
2
+
3
+ export interface SkillExecutionResult {
4
+ ok: boolean;
5
+ output: string;
6
+ }
7
+
8
+ /** Source origin of a skill — local builtin or MCP-backed. */
9
+ export type SkillSource = 'builtin' | 'mcp';
10
+ export type SkillGroup = `group:${string}`;
11
+
12
+ export interface Skill {
13
+ name: string;
14
+ description: string;
15
+ /** Native tool grouping (for example: group:fs, group:runtime). */
16
+ group?: SkillGroup;
17
+ /** Optional legacy aliases that should resolve to this canonical skill. */
18
+ aliases?: string[];
19
+ /** JSON Schema describing the tool's input parameters. */
20
+ parameters?: JsonSchema;
21
+ /** Where this skill originates from. @default 'builtin' */
22
+ source?: SkillSource;
23
+ /** If source is 'mcp', which server ID provides this tool. */
24
+ serverId?: string;
25
+ /** If source is 'mcp', the adapter instance for auditing/health. */
26
+ adapter?: McpScopeAuditAdapter;
27
+ /** If source is 'mcp', what capability scope this tool has. */
28
+ mcpScope?: McpCapabilityScope;
29
+ execute(input: Record<string, unknown>): Promise<SkillExecutionResult>;
30
+ }