@useconductor/conductor 1.0.0 → 1.0.1

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 (145) hide show
  1. package/.github/README.md +374 -7
  2. package/.github/workflows/ci.yml +3 -1
  3. package/.github/workflows/claude-code-review.yml +1 -15
  4. package/.github/workflows/publish.yml +43 -0
  5. package/README.md +290 -121
  6. package/dist/cli/commands/audit.d.ts +40 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -0
  8. package/dist/cli/commands/audit.js +272 -0
  9. package/dist/cli/commands/audit.js.map +1 -0
  10. package/dist/cli/commands/circuit.d.ts +13 -0
  11. package/dist/cli/commands/circuit.d.ts.map +1 -0
  12. package/dist/cli/commands/circuit.js +53 -0
  13. package/dist/cli/commands/circuit.js.map +1 -0
  14. package/dist/cli/commands/config.d.ts +31 -0
  15. package/dist/cli/commands/config.d.ts.map +1 -0
  16. package/dist/cli/commands/config.js +152 -0
  17. package/dist/cli/commands/config.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +5 -8
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +86 -123
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/cli/commands/marketplace.js +1 -1
  23. package/dist/cli/commands/onboard.d.ts.map +1 -1
  24. package/dist/cli/commands/onboard.js +33 -11
  25. package/dist/cli/commands/onboard.js.map +1 -1
  26. package/dist/cli/commands/release.d.ts.map +1 -1
  27. package/dist/cli/commands/release.js +1 -1
  28. package/dist/cli/commands/release.js.map +1 -1
  29. package/dist/cli/index.js +146 -10
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/core/audit.d.ts.map +1 -1
  32. package/dist/core/audit.js +5 -2
  33. package/dist/core/audit.js.map +1 -1
  34. package/dist/core/conductor.d.ts.map +1 -1
  35. package/dist/core/conductor.js +12 -0
  36. package/dist/core/conductor.js.map +1 -1
  37. package/dist/core/config.d.ts +3 -0
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +46 -2
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/database.d.ts +3 -0
  42. package/dist/core/database.d.ts.map +1 -1
  43. package/dist/core/database.js +26 -0
  44. package/dist/core/database.js.map +1 -1
  45. package/dist/core/encryption.d.ts +34 -0
  46. package/dist/core/encryption.d.ts.map +1 -0
  47. package/dist/core/encryption.js +96 -0
  48. package/dist/core/encryption.js.map +1 -0
  49. package/dist/core/zero-config.d.ts.map +1 -1
  50. package/dist/core/zero-config.js +1 -4
  51. package/dist/core/zero-config.js.map +1 -1
  52. package/dist/dashboard/server.d.ts.map +1 -1
  53. package/dist/dashboard/server.js +112 -16
  54. package/dist/dashboard/server.js.map +1 -1
  55. package/dist/mcp/server.d.ts.map +1 -1
  56. package/dist/mcp/server.js +30 -2
  57. package/dist/mcp/server.js.map +1 -1
  58. package/dist/plugins/builtin/aws.d.ts +31 -0
  59. package/dist/plugins/builtin/aws.d.ts.map +1 -0
  60. package/dist/plugins/builtin/aws.js +149 -0
  61. package/dist/plugins/builtin/aws.js.map +1 -0
  62. package/dist/plugins/builtin/database.d.ts +1 -0
  63. package/dist/plugins/builtin/database.d.ts.map +1 -1
  64. package/dist/plugins/builtin/database.js +26 -1
  65. package/dist/plugins/builtin/database.js.map +1 -1
  66. package/dist/plugins/builtin/docker.d.ts +4 -0
  67. package/dist/plugins/builtin/docker.d.ts.map +1 -1
  68. package/dist/plugins/builtin/docker.js +20 -1
  69. package/dist/plugins/builtin/docker.js.map +1 -1
  70. package/dist/plugins/builtin/gcp.d.ts +28 -0
  71. package/dist/plugins/builtin/gcp.d.ts.map +1 -0
  72. package/dist/plugins/builtin/gcp.js +135 -0
  73. package/dist/plugins/builtin/gcp.js.map +1 -0
  74. package/dist/plugins/builtin/index.d.ts.map +1 -1
  75. package/dist/plugins/builtin/index.js +4 -0
  76. package/dist/plugins/builtin/index.js.map +1 -1
  77. package/dist/plugins/builtin/jira.d.ts.map +1 -1
  78. package/dist/plugins/builtin/jira.js +4 -2
  79. package/dist/plugins/builtin/jira.js.map +1 -1
  80. package/dist/plugins/builtin/linear.js +1 -1
  81. package/dist/plugins/builtin/linear.js.map +1 -1
  82. package/dist/plugins/builtin/shell.js +1 -1
  83. package/dist/plugins/builtin/shell.js.map +1 -1
  84. package/dist/plugins/builtin/slack.d.ts +1 -0
  85. package/dist/plugins/builtin/slack.d.ts.map +1 -1
  86. package/dist/plugins/builtin/slack.js +9 -1
  87. package/dist/plugins/builtin/slack.js.map +1 -1
  88. package/dist/plugins/builtin/spotify.js +1 -1
  89. package/dist/plugins/builtin/spotify.js.map +1 -1
  90. package/dist/plugins/builtin/vercel.d.ts.map +1 -1
  91. package/dist/plugins/builtin/vercel.js +3 -1
  92. package/dist/plugins/builtin/vercel.js.map +1 -1
  93. package/dist/security/sso.d.ts +37 -0
  94. package/dist/security/sso.d.ts.map +1 -0
  95. package/dist/security/sso.js +92 -0
  96. package/dist/security/sso.js.map +1 -0
  97. package/docs/deployment.md +201 -0
  98. package/docs/plugin-sdk.md +212 -0
  99. package/package.json +11 -8
  100. package/src/cli/commands/audit.ts +318 -0
  101. package/src/cli/commands/circuit.ts +63 -0
  102. package/src/cli/commands/config.ts +176 -0
  103. package/src/cli/commands/init.ts +87 -145
  104. package/src/cli/commands/marketplace.ts +1 -1
  105. package/src/cli/commands/onboard.ts +33 -11
  106. package/src/cli/commands/release.ts +13 -6
  107. package/src/cli/index.ts +165 -11
  108. package/src/core/audit.ts +5 -2
  109. package/src/core/conductor.ts +11 -0
  110. package/src/core/config.ts +47 -2
  111. package/src/core/database.ts +32 -0
  112. package/src/core/encryption.ts +110 -0
  113. package/src/core/zero-config.ts +1 -5
  114. package/src/dashboard/server.ts +135 -16
  115. package/src/mcp/server.ts +40 -2
  116. package/src/plugins/builtin/aws.ts +162 -0
  117. package/src/plugins/builtin/database.ts +19 -1
  118. package/src/plugins/builtin/docker.ts +17 -1
  119. package/src/plugins/builtin/gcp.ts +145 -0
  120. package/src/plugins/builtin/index.ts +4 -0
  121. package/src/plugins/builtin/jira.ts +23 -19
  122. package/src/plugins/builtin/linear.ts +1 -1
  123. package/src/plugins/builtin/shell.ts +1 -1
  124. package/src/plugins/builtin/slack.ts +6 -1
  125. package/src/plugins/builtin/spotify.ts +1 -1
  126. package/src/plugins/builtin/vercel.ts +3 -1
  127. package/src/security/sso.ts +124 -0
  128. package/tests/audit.test.ts +185 -0
  129. package/tests/circuit-breaker.test.ts +125 -0
  130. package/tests/docker.test.ts +244 -39
  131. package/tests/errors.test.ts +122 -0
  132. package/tests/github.test.ts.skip +392 -0
  133. package/tests/jira.test.ts +310 -0
  134. package/tests/linear.test.ts +366 -0
  135. package/tests/mcp.test.ts.skip +243 -0
  136. package/tests/notion.test.ts +257 -0
  137. package/tests/retry.test.ts +104 -0
  138. package/tests/shell.test.ts +262 -30
  139. package/tests/slack.test.ts +250 -0
  140. package/tests/stripe.test.ts +272 -0
  141. package/tests/validation.test.ts +173 -0
  142. package/tests/vercel.test.ts +368 -0
  143. package/tests/zero-config.test.ts +566 -0
  144. package/C.png +0 -0
  145. package/tests/mcp.test.ts +0 -14
@@ -0,0 +1,212 @@
1
+ # Plugin SDK
2
+
3
+ Create your own plugins for Conductor.
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ import { Plugin, PluginTool } from '@useconductor/conductor/plugins';
9
+
10
+ export class MyPlugin implements Plugin {
11
+ name = 'my-plugin';
12
+ description = 'My custom plugin';
13
+ version = '1.0.0';
14
+
15
+ async initialize(conductor) {
16
+ // Setup - e.g., connect to API, load config
17
+ }
18
+
19
+ isConfigured(): boolean {
20
+ // Return true if plugin has required credentials
21
+ return true;
22
+ }
23
+
24
+ getTools(): PluginTool[] {
25
+ return [
26
+ {
27
+ name: 'my_plugin_action',
28
+ description: 'Does something useful',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ input: { type: 'string', description: 'Input description' }
33
+ },
34
+ required: ['input']
35
+ },
36
+ handler: async (args) => {
37
+ const result = await this.doSomething(args.input);
38
+ return { result };
39
+ }
40
+ }
41
+ ];
42
+ }
43
+
44
+ // Optional: proactive context for AI
45
+ async getContext() {
46
+ return null;
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Full Example
52
+
53
+ ```typescript
54
+ import { Plugin, PluginTool, ToolContext } from '@useconductor/conductor/plugins';
55
+
56
+ export class GitHub IssuesPlugin implements Plugin {
57
+ name = 'github-issues';
58
+ description = 'Manage GitHub issues';
59
+ version = '1.0.0';
60
+
61
+ private apiKey?: string;
62
+ private owner?: string;
63
+ private repo?: string;
64
+
65
+ async initialize(conductor) {
66
+ const config = conductor.getConfig();
67
+ this.apiKey = await conductor.getKeychain().get('github', 'token');
68
+
69
+ const prefs = config.get('plugins.github-issues');
70
+ this.owner = prefs?.owner;
71
+ this.repo = prefs?.repo;
72
+ }
73
+
74
+ isConfigured(): boolean {
75
+ return !!this.apiKey && !!this.owner && !!this.repo;
76
+ }
77
+
78
+ getTools(): PluginTool[] {
79
+ return [
80
+ {
81
+ name: 'github_issues_list',
82
+ description: 'List GitHub issues',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ state: {
87
+ type: 'string',
88
+ enum: ['open', 'closed', 'all'],
89
+ description: 'Issue state'
90
+ },
91
+ limit: {
92
+ type: 'number',
93
+ description: 'Max issues to return'
94
+ }
95
+ }
96
+ },
97
+ handler: async (args, context) => {
98
+ return this.listIssues(args.state, args.limit);
99
+ }
100
+ },
101
+ {
102
+ name: 'github_issues_create',
103
+ description: 'Create a GitHub issue',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ title: { type: 'string' },
108
+ body: { type: 'string' },
109
+ labels: { type: 'array', items: { type: 'string' } }
110
+ },
111
+ required: ['title']
112
+ },
113
+ handler: async (args) => {
114
+ return this.createIssue(args.title, args.body, args.labels);
115
+ }
116
+ }
117
+ ];
118
+ }
119
+
120
+ private async listIssues(state: string = 'open', limit: number = 10) {
121
+ // Implementation
122
+ }
123
+
124
+ private async createIssue(title: string, body?: string, labels?: string[]) {
125
+ // Implementation
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Config Schema
131
+
132
+ For `conductor plugins setup <name>`:
133
+
134
+ ```typescript
135
+ getConfigSchema() {
136
+ return {
137
+ fields: {
138
+ owner: {
139
+ label: 'GitHub Owner',
140
+ type: 'string',
141
+ required: true,
142
+ description: 'Organization or username'
143
+ },
144
+ repo: {
145
+ label: 'Repository',
146
+ type: 'string',
147
+ required: true
148
+ },
149
+ token: {
150
+ label: 'GitHub Token',
151
+ type: 'password',
152
+ secret: true,
153
+ description: 'Personal access token'
154
+ }
155
+ }
156
+ };
157
+ }
158
+ ```
159
+
160
+ ## Tool Handler Signature
161
+
162
+ ```typescript
163
+ handler: async (
164
+ args: Record<string, unknown>, // Parsed input
165
+ context: ToolContext // Execution context
166
+ ) => {
167
+ // args = parsed and validated input
168
+ // context.conductor = Conductor instance
169
+ // context.user = user info (in multi-user mode)
170
+
171
+ return { /* result */ };
172
+ }
173
+ ```
174
+
175
+ ## Publishing
176
+
177
+ ```bash
178
+ # Build
179
+ npm run build
180
+
181
+ # Publish to npm
182
+ npm publish
183
+
184
+ # Or submit to Conductor marketplace
185
+ conductor plugins publish ./dist
186
+ ```
187
+
188
+ ## Types
189
+
190
+ ```typescript
191
+ interface Plugin {
192
+ name: string;
193
+ description: string;
194
+ version: string;
195
+
196
+ initialize(conductor: Conductor): Promise<void>;
197
+ isConfigured(): boolean;
198
+ getTools(): PluginTool[];
199
+
200
+ // Optional
201
+ getConfigSchema?(): PluginConfigSchema;
202
+ getContext?(): Promise<string | null>;
203
+ }
204
+
205
+ interface PluginTool {
206
+ name: string;
207
+ description: string;
208
+ inputSchema: object;
209
+ handler: (args: any, context: ToolContext) => Promise<any>;
210
+ requiresApproval?: boolean;
211
+ }
212
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@useconductor/conductor",
3
- "version": "1.0.0",
4
- "description": "The AI Tool Hub — One MCP server. 100+ tools. Every AI agent.",
3
+ "version": "1.0.1",
4
+ "description": "The AI Tool Hub — One MCP server. 255 tools. Every AI agent.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "conductor": "./dist/cli/index.js"
@@ -19,14 +19,16 @@
19
19
  "format": "prettier --write \"src/**/*.ts\"",
20
20
  "format:check": "prettier --check \"src/**/*.ts\"",
21
21
  "typecheck": "tsc --noEmit",
22
- "prepublishOnly": "npm run build"
22
+ "prepublishOnly": "npm run build",
23
+ "publish": "npm run build && npm publish",
24
+ "publish:beta": "npm run build && npm publish --tag beta"
23
25
  },
24
26
  "publishConfig": {
25
27
  "access": "public",
26
28
  "registry": "https://registry.npmjs.org/"
27
29
  },
28
30
  "engines": {
29
- "node": ">=18.0.0"
31
+ "node": ">=20.12.0"
30
32
  },
31
33
  "keywords": [
32
34
  "mcp",
@@ -44,8 +46,6 @@
44
46
  "license": "Apache-2.0",
45
47
  "type": "module",
46
48
  "dependencies": {
47
- "@anthropic-ai/sdk": "^0.74.0",
48
- "@google/generative-ai": "^0.24.1",
49
49
  "@modelcontextprotocol/sdk": "^1.26.0",
50
50
  "@octokit/rest": "^22.0.1",
51
51
  "@slack/bolt": "^4.6.0",
@@ -59,7 +59,6 @@
59
59
  "inquirer": "^13.2.5",
60
60
  "mathjs": "^15.1.1",
61
61
  "open": "^11.0.0",
62
- "openai": "^6.22.0",
63
62
  "ora": "^9.3.0",
64
63
  "pino": "^10.3.1",
65
64
  "pino-pretty": "^13.1.3",
@@ -84,7 +83,11 @@
84
83
  "prettier": "^3.8.1",
85
84
  "tsx": "^4.21.0",
86
85
  "typescript": "^5.9.3",
87
- "vitepress": "^1.6.4",
88
86
  "vitest": "^4.1.2"
87
+ },
88
+ "optionalDependencies": {
89
+ "@anthropic-ai/sdk": "^0.86.1",
90
+ "@google/generative-ai": "^0.24.1",
91
+ "openai": "^6.34.0"
89
92
  }
90
93
  }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * conductor audit — query and verify the tamper-evident audit log.
3
+ *
4
+ * Commands:
5
+ * conductor audit list — filter and display log entries
6
+ * conductor audit verify — verify SHA-256 chain integrity
7
+ * conductor audit tail — stream the log in real time
8
+ * conductor audit export — export entries to JSON or NDJSON
9
+ * conductor audit stats — show summary statistics
10
+ * conductor audit rotate — manually rotate the current log file
11
+ */
12
+
13
+ import fs from 'fs/promises';
14
+ import { createReadStream } from 'fs';
15
+ import path from 'path';
16
+ import readline from 'readline';
17
+ import { AuditLogger } from '../../core/audit.js';
18
+ import type { AuditEntry } from '../../core/audit.js';
19
+ import type { Conductor } from '../../core/conductor.js';
20
+
21
+ function getAuditDir(conductor: Conductor): string {
22
+ return path.join(conductor.getConfig().getConfigDir(), 'audit');
23
+ }
24
+
25
+ function getAuditFile(conductor: Conductor): string {
26
+ return path.join(getAuditDir(conductor), 'audit.log');
27
+ }
28
+
29
+ /** Read all entries from all audit log files, newest files last. */
30
+ async function readAllEntries(conductor: Conductor): Promise<AuditEntry[]> {
31
+ const dir = getAuditDir(conductor);
32
+ const entries: AuditEntry[] = [];
33
+
34
+ let files: string[];
35
+ try {
36
+ files = await fs.readdir(dir);
37
+ } catch {
38
+ return [];
39
+ }
40
+
41
+ const logFiles = files.filter((f) => f.endsWith('.log')).sort();
42
+
43
+ for (const file of logFiles) {
44
+ const content = await fs.readFile(path.join(dir, file), 'utf-8').catch(() => '');
45
+ for (const line of content.split('\n').filter((l) => l.trim())) {
46
+ try {
47
+ entries.push(JSON.parse(line) as AuditEntry);
48
+ } catch {
49
+ // skip malformed lines
50
+ }
51
+ }
52
+ }
53
+
54
+ return entries;
55
+ }
56
+
57
+ function formatEntry(e: AuditEntry): string {
58
+ const time = e.timestamp.replace('T', ' ').replace(/\.\d+Z$/, '');
59
+ const icon = e.result === 'success' ? '✓' : e.result === 'failure' ? '✗' : e.result === 'denied' ? '⊘' : '⏱';
60
+ return ` ${icon} ${time} ${e.actor.padEnd(12)} ${e.action.padEnd(16)} ${e.resource}`;
61
+ }
62
+
63
+ // ── list ──────────────────────────────────────────────────────────────────────
64
+
65
+ export async function auditList(
66
+ conductor: Conductor,
67
+ opts: {
68
+ actor?: string;
69
+ action?: string;
70
+ tool?: string;
71
+ result?: string;
72
+ since?: string;
73
+ until?: string;
74
+ limit?: string;
75
+ json?: boolean;
76
+ },
77
+ ): Promise<void> {
78
+ let entries = await readAllEntries(conductor);
79
+
80
+ if (opts.actor) entries = entries.filter((e) => e.actor === opts.actor);
81
+ if (opts.action) entries = entries.filter((e) => e.action === opts.action);
82
+ if (opts.tool) entries = entries.filter((e) => e.resource === opts.tool);
83
+ if (opts.result) entries = entries.filter((e) => e.result === opts.result);
84
+ if (opts.since) entries = entries.filter((e) => e.timestamp >= opts.since!);
85
+ if (opts.until) entries = entries.filter((e) => e.timestamp <= opts.until!);
86
+
87
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 100;
88
+ entries = entries.slice(-limit);
89
+
90
+ if (entries.length === 0) {
91
+ console.log('\n No audit entries found.\n');
92
+ return;
93
+ }
94
+
95
+ if (opts.json) {
96
+ console.log(JSON.stringify(entries, null, 2));
97
+ return;
98
+ }
99
+
100
+ console.log('');
101
+ console.log(` 📋 Audit Log (${entries.length} entries)\n`);
102
+ console.log(
103
+ ` ${'RESULT'.padEnd(3)} ${'TIMESTAMP'.padEnd(19)} ${'ACTOR'.padEnd(12)} ${'ACTION'.padEnd(16)} RESOURCE`,
104
+ );
105
+ console.log(' ' + '─'.repeat(80));
106
+ for (const e of entries) {
107
+ console.log(formatEntry(e));
108
+ }
109
+ console.log('');
110
+ }
111
+
112
+ // ── verify ────────────────────────────────────────────────────────────────────
113
+
114
+ export async function auditVerify(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
115
+ const logger = new AuditLogger(conductor.getConfig().getConfigDir(), { flushIntervalMs: 50000 });
116
+ try {
117
+ const { valid, brokenAt } = await logger.verifyIntegrity();
118
+
119
+ if (opts.json) {
120
+ console.log(JSON.stringify({ valid, brokenAt }));
121
+ return;
122
+ }
123
+
124
+ console.log('');
125
+ if (valid) {
126
+ console.log(' ✅ Audit log integrity verified — no tampering detected.\n');
127
+ } else {
128
+ console.log(` ❌ Integrity check FAILED — chain broken at: ${brokenAt}\n`);
129
+ console.log(' The audit log may have been tampered with. Contact your security team.\n');
130
+ process.exit(1);
131
+ }
132
+ } finally {
133
+ await logger.close();
134
+ }
135
+ }
136
+
137
+ // ── tail ──────────────────────────────────────────────────────────────────────
138
+
139
+ export async function auditTail(conductor: Conductor, opts: { json?: boolean; lines?: string }): Promise<void> {
140
+ const logFile = getAuditFile(conductor);
141
+ const initialLines = parseInt(opts.lines || '20', 10);
142
+
143
+ // Show last N lines from existing file
144
+ try {
145
+ const content = await fs.readFile(logFile, 'utf-8');
146
+ const lines = content.split('\n').filter((l) => l.trim());
147
+ const recent = lines.slice(-initialLines);
148
+
149
+ console.log('');
150
+ for (const line of recent) {
151
+ try {
152
+ const e = JSON.parse(line) as AuditEntry;
153
+ if (opts.json) {
154
+ console.log(JSON.stringify(e));
155
+ } else {
156
+ console.log(formatEntry(e));
157
+ }
158
+ } catch {
159
+ // skip
160
+ }
161
+ }
162
+ } catch {
163
+ console.log('\n No audit log found yet.\n');
164
+ }
165
+
166
+ // Watch for new lines
167
+ console.log('\n Watching for new entries (Ctrl+C to stop)...\n');
168
+
169
+ let fileSize = 0;
170
+ try {
171
+ const stat = await fs.stat(logFile);
172
+ fileSize = stat.size;
173
+ } catch {
174
+ fileSize = 0;
175
+ }
176
+
177
+ const watcher = setInterval(async () => {
178
+ try {
179
+ const stat = await fs.stat(logFile);
180
+ if (stat.size > fileSize) {
181
+ const stream = createReadStream(logFile, { start: fileSize });
182
+ const rl = readline.createInterface({ input: stream });
183
+ rl.on('line', (line) => {
184
+ if (!line.trim()) return;
185
+ try {
186
+ const e = JSON.parse(line) as AuditEntry;
187
+ if (opts.json) {
188
+ console.log(JSON.stringify(e));
189
+ } else {
190
+ console.log(formatEntry(e));
191
+ }
192
+ } catch {
193
+ // skip
194
+ }
195
+ });
196
+ fileSize = stat.size;
197
+ }
198
+ } catch {
199
+ // file not yet created
200
+ }
201
+ }, 500);
202
+
203
+ process.on('SIGINT', () => {
204
+ clearInterval(watcher);
205
+ console.log('\n');
206
+ process.exit(0);
207
+ });
208
+
209
+ // Keep alive
210
+ await new Promise(() => {});
211
+ }
212
+
213
+ // ── export ────────────────────────────────────────────────────────────────────
214
+
215
+ export async function auditExport(
216
+ conductor: Conductor,
217
+ opts: {
218
+ output?: string;
219
+ format?: string;
220
+ since?: string;
221
+ until?: string;
222
+ },
223
+ ): Promise<void> {
224
+ let entries = await readAllEntries(conductor);
225
+
226
+ if (opts.since) entries = entries.filter((e) => e.timestamp >= opts.since!);
227
+ if (opts.until) entries = entries.filter((e) => e.timestamp <= opts.until!);
228
+
229
+ const format = opts.format || 'json';
230
+ const output = opts.output || `-`;
231
+
232
+ let content: string;
233
+ if (format === 'ndjson') {
234
+ content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
235
+ } else {
236
+ content = JSON.stringify(entries, null, 2) + '\n';
237
+ }
238
+
239
+ if (output === '-') {
240
+ process.stdout.write(content);
241
+ } else {
242
+ await fs.writeFile(output, content, 'utf-8');
243
+ console.log(`\n ✅ Exported ${entries.length} entries to: ${output}\n`);
244
+ }
245
+ }
246
+
247
+ // ── stats ─────────────────────────────────────────────────────────────────────
248
+
249
+ export async function auditStats(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
250
+ const entries = await readAllEntries(conductor);
251
+
252
+ if (entries.length === 0) {
253
+ console.log('\n No audit entries found.\n');
254
+ return;
255
+ }
256
+
257
+ const byAction: Record<string, number> = {};
258
+ const byActor: Record<string, number> = {};
259
+ const byResult: Record<string, number> = {};
260
+
261
+ for (const e of entries) {
262
+ byAction[e.action] = (byAction[e.action] || 0) + 1;
263
+ byActor[e.actor] = (byActor[e.actor] || 0) + 1;
264
+ byResult[e.result] = (byResult[e.result] || 0) + 1;
265
+ }
266
+
267
+ const stats = {
268
+ total: entries.length,
269
+ first: entries[0]?.timestamp,
270
+ last: entries[entries.length - 1]?.timestamp,
271
+ by_action: byAction,
272
+ by_actor: byActor,
273
+ by_result: byResult,
274
+ };
275
+
276
+ if (opts.json) {
277
+ console.log(JSON.stringify(stats, null, 2));
278
+ return;
279
+ }
280
+
281
+ console.log('');
282
+ console.log(` 📊 Audit Log Statistics\n`);
283
+ console.log(` Total entries: ${stats.total}`);
284
+ console.log(` First entry: ${stats.first}`);
285
+ console.log(` Last entry: ${stats.last}`);
286
+ console.log('');
287
+ console.log(' By result:');
288
+ for (const [k, v] of Object.entries(byResult).sort((a, b) => b[1] - a[1])) {
289
+ console.log(` ${k.padEnd(12)} ${v}`);
290
+ }
291
+ console.log('');
292
+ console.log(' By action (top 10):');
293
+ for (const [k, v] of Object.entries(byAction)
294
+ .sort((a, b) => b[1] - a[1])
295
+ .slice(0, 10)) {
296
+ console.log(` ${k.padEnd(20)} ${v}`);
297
+ }
298
+ console.log('');
299
+ }
300
+
301
+ // ── rotate ────────────────────────────────────────────────────────────────────
302
+
303
+ export async function auditRotate(conductor: Conductor): Promise<void> {
304
+ const logFile = getAuditFile(conductor);
305
+
306
+ try {
307
+ const stat = await fs.stat(logFile);
308
+ if (stat.size === 0) {
309
+ console.log('\n Log file is empty — nothing to rotate.\n');
310
+ return;
311
+ }
312
+ const rotated = `${logFile}.${Date.now()}.bak`;
313
+ await fs.rename(logFile, rotated);
314
+ console.log(`\n ✅ Rotated audit log to: ${path.basename(rotated)}\n`);
315
+ } catch {
316
+ console.log('\n No audit log to rotate.\n');
317
+ }
318
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * conductor circuit — view and manage circuit breaker state.
3
+ *
4
+ * Commands:
5
+ * conductor circuit list — show state of all circuit breakers
6
+ * conductor circuit reset — reset a specific circuit to closed state
7
+ */
8
+
9
+ import type { Conductor } from '../../core/conductor.js';
10
+
11
+ export async function circuitList(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
12
+ await conductor.initialize();
13
+
14
+ // Circuit breaker state is held in the running MCP server's memory.
15
+ // If we're not in the server process, we read the persisted health state
16
+ // from the health check endpoint instead.
17
+ try {
18
+ const { HealthChecker } = await import('../../core/health.js');
19
+ const checker = new HealthChecker();
20
+ const report = await checker.detailed('0');
21
+
22
+ const metrics = report.metrics;
23
+
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({ open_circuits: metrics?.openCircuits ?? 0 }, null, 2));
26
+ return;
27
+ }
28
+
29
+ console.log('');
30
+ console.log(' ⚡ Circuit Breaker Status\n');
31
+
32
+ if (!metrics || metrics.openCircuits === 0) {
33
+ console.log(' All circuits are CLOSED (healthy).\n');
34
+ } else {
35
+ console.log(
36
+ ` ⚠️ ${metrics.openCircuits} circuit(s) OPEN — tools unavailable until recovery timeout expires.\n`,
37
+ );
38
+ console.log(' Run: conductor health --json for per-tool details.\n');
39
+ }
40
+
41
+ console.log(` Total tool calls: ${metrics?.totalToolCalls ?? 0}`);
42
+ console.log(` Failed calls: ${metrics?.failedToolCalls ?? 0}`);
43
+ console.log(` Avg latency: ${metrics?.avgLatencyMs ?? 0}ms`);
44
+ console.log('');
45
+ console.log(' To reset a specific circuit: conductor circuit reset <tool>\n');
46
+ } catch (e: unknown) {
47
+ const msg = e instanceof Error ? e.message : String(e);
48
+ console.error(` ❌ ${msg}\n`);
49
+ }
50
+ }
51
+
52
+ export async function circuitReset(conductor: Conductor, tool: string): Promise<void> {
53
+ await conductor.initialize();
54
+
55
+ // Circuit breaker reset requires the server process. We emit a signal file
56
+ // that the running server will pick up, or print guidance if no server is running.
57
+ console.log('');
58
+ console.log(` ℹ️ Circuit breaker state lives in the running MCP server process.`);
59
+ console.log(` To reset "${tool}":`);
60
+ console.log(` 1. Restart the MCP server: conductor mcp start`);
61
+ console.log(` 2. Or wait for the recovery timeout to expire (default: 30s)`);
62
+ console.log('');
63
+ }