slaunt 0.1.16 → 0.1.18
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 +17 -106
- package/bin/slaunt-hooks.js +96 -0
- package/package.json +9 -59
- package/release/SlauntKillBar.zip +0 -0
- package/release/manifest.json +7 -0
- package/scripts/killbar-portability-check.js +67 -0
- package/scripts/package-killbar-macos.sh +38 -0
- package/src/approval-manager.js +53 -0
- package/src/cli-auth.js +230 -0
- package/src/compact-wordmark.js +13 -0
- package/src/desktop/broker-daemon.js +18 -0
- package/src/desktop/broker-grants.js +37 -0
- package/src/desktop/broker-hook-command.js +23 -0
- package/src/desktop/broker-keychain.js +29 -0
- package/src/desktop/broker-launch-agent.js +34 -0
- package/src/desktop/broker-service.js +40 -0
- package/src/desktop/chat-capture-command.js +7 -0
- package/src/desktop/chat-hook-capture.js +445 -0
- package/src/desktop/claude-lifecycle.js +143 -0
- package/src/desktop/client-identity.js +19 -0
- package/src/desktop/native-claude-broker.js +34 -0
- package/src/desktop/native-claude-connection.js +42 -0
- package/src/desktop/tool-guard-command.js +13 -0
- package/src/desktop/tool-guard.js +225 -0
- package/src/discovery/catalog.js +734 -0
- package/src/full-screen.js +88 -0
- package/src/helper-release.js +27 -0
- package/src/hidden-oauth.js +70 -0
- package/src/independent-grants.js +38 -0
- package/src/install-flow.js +71 -0
- package/src/killbar-background-install.js +47 -0
- package/src/killbar-enrollment.js +74 -0
- package/src/killbar-launch-agent.js +10 -0
- package/src/mcp-identity.js +6 -0
- package/src/native-config.js +150 -0
- package/src/native-oauth.js +33 -0
- package/src/oauth-cohort.js +43 -0
- package/src/pty-wrapper.py +57 -0
- package/src/read-json.js +8 -0
- package/src/setup/client-installer.js +93 -0
- package/src/setup/codex-guard-supervisor.js +34 -0
- package/src/setup/hook-installer.js +496 -0
- package/src/setup/managed-policy.js +26 -0
- package/src/setup/mcp-config-installer.js +204 -0
- package/src/setup-flow.js +66 -0
- package/src/tui.js +205 -0
- package/test/approval-manager.test.js +27 -0
- package/test/broker-grants.test.js +14 -0
- package/test/broker-keychain.test.js +14 -0
- package/test/broker-launch-agent.test.js +15 -0
- package/test/broker-service.test.js +42 -0
- package/test/cli-auth-recovery.test.js +15 -0
- package/test/codex-guard-supervisor.test.js +25 -0
- package/test/full-screen.test.js +49 -0
- package/test/helper-release.test.js +22 -0
- package/test/hidden-oauth.test.js +44 -0
- package/test/hook-installer.test.js +232 -0
- package/test/independent-grants.test.js +16 -0
- package/test/install-flow.test.js +24 -0
- package/test/killbar-background-install.test.js +15 -0
- package/test/killbar-enrollment.test.js +115 -0
- package/test/killbar-launch-agent.test.js +12 -0
- package/test/killbar-portability.test.js +17 -0
- package/test/managed-policy.test.js +18 -0
- package/test/native-claude-broker.test.js +19 -0
- package/test/native-claude-connection.test.js +17 -0
- package/test/native-claude-hooks.test.js +9 -0
- package/test/native-config.test.js +3 -0
- package/test/native-integration.cjs +29 -0
- package/test/native-oauth.test.js +23 -0
- package/test/oauth-cohort.test.js +26 -0
- package/test/tui.test.js +100 -0
- package/LICENSE +0 -201
- package/dist/audit/risk-engine.d.ts +0 -4
- package/dist/audit/risk-engine.d.ts.map +0 -1
- package/dist/audit/risk-engine.js +0 -174
- package/dist/audit/risk-engine.js.map +0 -1
- package/dist/audit/run.d.ts +0 -16
- package/dist/audit/run.d.ts.map +0 -1
- package/dist/audit/run.js +0 -66
- package/dist/audit/run.js.map +0 -1
- package/dist/classification/builtin-catalog.d.ts +0 -3
- package/dist/classification/builtin-catalog.d.ts.map +0 -1
- package/dist/classification/builtin-catalog.js +0 -283
- package/dist/classification/builtin-catalog.js.map +0 -1
- package/dist/classification/catalog.d.ts +0 -10
- package/dist/classification/catalog.d.ts.map +0 -1
- package/dist/classification/catalog.js +0 -194
- package/dist/classification/catalog.js.map +0 -1
- package/dist/classification/classifier.d.ts +0 -3
- package/dist/classification/classifier.d.ts.map +0 -1
- package/dist/classification/classifier.js +0 -64
- package/dist/classification/classifier.js.map +0 -1
- package/dist/classification/public-catalog-config.d.ts +0 -3
- package/dist/classification/public-catalog-config.d.ts.map +0 -1
- package/dist/classification/public-catalog-config.js +0 -5
- package/dist/classification/public-catalog-config.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -220
- package/dist/cli.js.map +0 -1
- package/dist/config/discover.d.ts +0 -8
- package/dist/config/discover.d.ts.map +0 -1
- package/dist/config/discover.js +0 -163
- package/dist/config/discover.js.map +0 -1
- package/dist/config/install.d.ts +0 -10
- package/dist/config/install.d.ts.map +0 -1
- package/dist/config/install.js +0 -143
- package/dist/config/install.js.map +0 -1
- package/dist/config/paths.d.ts +0 -24
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/paths.js +0 -90
- package/dist/config/paths.js.map +0 -1
- package/dist/demo.d.ts +0 -4
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -99
- package/dist/demo.js.map +0 -1
- package/dist/explanation/local.d.ts +0 -24
- package/dist/explanation/local.d.ts.map +0 -1
- package/dist/explanation/local.js +0 -667
- package/dist/explanation/local.js.map +0 -1
- package/dist/mcp/introspect.d.ts +0 -14
- package/dist/mcp/introspect.d.ts.map +0 -1
- package/dist/mcp/introspect.js +0 -186
- package/dist/mcp/introspect.js.map +0 -1
- package/dist/presentation/plain-language.d.ts +0 -12
- package/dist/presentation/plain-language.d.ts.map +0 -1
- package/dist/presentation/plain-language.js +0 -120
- package/dist/presentation/plain-language.js.map +0 -1
- package/dist/privacy/redaction.d.ts +0 -2
- package/dist/privacy/redaction.d.ts.map +0 -1
- package/dist/privacy/redaction.js +0 -50
- package/dist/privacy/redaction.js.map +0 -1
- package/dist/report/html.d.ts +0 -4
- package/dist/report/html.d.ts.map +0 -1
- package/dist/report/html.js +0 -101
- package/dist/report/html.js.map +0 -1
- package/dist/tui/analysis-path.d.ts +0 -17
- package/dist/tui/analysis-path.d.ts.map +0 -1
- package/dist/tui/analysis-path.js +0 -184
- package/dist/tui/analysis-path.js.map +0 -1
- package/dist/tui/render.d.ts +0 -4
- package/dist/tui/render.d.ts.map +0 -1
- package/dist/tui/render.js +0 -273
- package/dist/tui/render.js.map +0 -1
- package/dist/tui/splash.d.ts +0 -15
- package/dist/tui/splash.d.ts.map +0 -1
- package/dist/tui/splash.js +0 -130
- package/dist/tui/splash.js.map +0 -1
- package/dist/tui/stream.d.ts +0 -6
- package/dist/tui/stream.d.ts.map +0 -1
- package/dist/tui/stream.js +0 -56
- package/dist/tui/stream.js.map +0 -1
- package/dist/types.d.ts +0 -131
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -8
- package/dist/version.js.map +0 -1
- package/models/APACHE-2.0.txt +0 -202
- package/models/MODEL.json +0 -29
- package/models/NOTICE.md +0 -23
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { runHiddenLogin } = require('./hidden-oauth');
|
|
3
|
+
// The authority must show the user the selected clients and approve the exact
|
|
4
|
+
// requests. It is injected because the Slaunt endpoint is not yet available.
|
|
5
|
+
async function authorizeCohort({ clients, issuer, approve, login = runHiddenLogin, executablePaths = {}, timeoutMs = 120000 }) {
|
|
6
|
+
if (!Array.isArray(clients) || clients.length < 1 || clients.length > 2 || new Set(clients).size !== clients.length || clients.some(c => !['codex','claude'].includes(c)) || typeof approve !== 'function') throw new Error('invalid-cohort');
|
|
7
|
+
const selected = new Set(clients);
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
let started = false;
|
|
10
|
+
let failed = false;
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
let timer;
|
|
13
|
+
let rejectOverall;
|
|
14
|
+
const failedPromise = new Promise((_, reject) => { rejectOverall = reject; });
|
|
15
|
+
const fail = (stage, client, detail) => {
|
|
16
|
+
if (failed) return;
|
|
17
|
+
failed = true;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
controller.abort();
|
|
20
|
+
for (const entry of pending.values()) entry.reject(new Error('cohort-failed'));
|
|
21
|
+
const error = new Error('cohort-failed');
|
|
22
|
+
error.setupStage = stage;
|
|
23
|
+
if (['sign-in','workspace','enrollment','client-grants'].includes(detail)) error.setupDetail = detail;
|
|
24
|
+
if (client === 'codex' || client === 'claude') error.setupClient = client;
|
|
25
|
+
rejectOverall(error);
|
|
26
|
+
};
|
|
27
|
+
timer = setTimeout(() => fail('timeout'), timeoutMs);
|
|
28
|
+
const authorize = ({client, authorizationUrl}) => new Promise((resolve, reject) => {
|
|
29
|
+
if (failed || !selected.has(client) || pending.has(client) || typeof authorizationUrl !== 'string') { reject(new Error('cohort-invalid-request')); fail('authorization'); return; }
|
|
30
|
+
pending.set(client, { authorizationUrl, resolve, reject });
|
|
31
|
+
if (pending.size !== clients.length || started) return;
|
|
32
|
+
started = true;
|
|
33
|
+
const requests = clients.map(name => ({client:name, authorizationUrl:pending.get(name).authorizationUrl}));
|
|
34
|
+
Promise.resolve().then(() => approve(requests)).then(callbacks => {
|
|
35
|
+
if (failed) return;
|
|
36
|
+
if (!callbacks || typeof callbacks !== 'object' || Array.isArray(callbacks) || Object.keys(callbacks).length !== clients.length || clients.some(name => typeof callbacks[name] !== 'string')) throw new Error('cohort-invalid-response');
|
|
37
|
+
for (const name of clients) pending.get(name).resolve(callbacks[name]);
|
|
38
|
+
}).catch(error => fail('authorization', undefined, error?.setupDetail));
|
|
39
|
+
});
|
|
40
|
+
const runs = clients.map(executable => Promise.resolve().then(() => login({executable,commandPath:executablePaths[executable] || executable,issuer,authorize,timeoutMs,signal:controller.signal})).catch(error => { fail('native-login', executable); throw error; }));
|
|
41
|
+
try { await Promise.race([Promise.all(runs), failedPromise]); } finally { clearTimeout(timer); }
|
|
42
|
+
}
|
|
43
|
+
module.exports = { authorizeCohort };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pty
|
|
3
|
+
import select
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import termios
|
|
8
|
+
|
|
9
|
+
master, slave = pty.openpty()
|
|
10
|
+
attrs = termios.tcgetattr(slave)
|
|
11
|
+
attrs[3] &= ~termios.ECHO
|
|
12
|
+
termios.tcsetattr(slave, termios.TCSANOW, attrs)
|
|
13
|
+
process = subprocess.Popen(sys.argv[1:], stdin=slave, stdout=slave, stderr=slave, start_new_session=True)
|
|
14
|
+
os.close(slave)
|
|
15
|
+
|
|
16
|
+
def stop(_signal, _frame):
|
|
17
|
+
raise KeyboardInterrupt()
|
|
18
|
+
|
|
19
|
+
signal.signal(signal.SIGTERM, stop)
|
|
20
|
+
signal.signal(signal.SIGINT, stop)
|
|
21
|
+
stdin_open = True
|
|
22
|
+
interrupted = False
|
|
23
|
+
try:
|
|
24
|
+
while True:
|
|
25
|
+
sources = [master] + ([sys.stdin.buffer] if stdin_open else [])
|
|
26
|
+
ready, _, _ = select.select(sources, [], [], 0.2)
|
|
27
|
+
if master in ready:
|
|
28
|
+
try:
|
|
29
|
+
data = os.read(master, 4096)
|
|
30
|
+
except OSError:
|
|
31
|
+
data = b''
|
|
32
|
+
if not data:
|
|
33
|
+
break
|
|
34
|
+
os.write(sys.stdout.fileno(), data)
|
|
35
|
+
if stdin_open and sys.stdin.buffer in ready:
|
|
36
|
+
data = os.read(sys.stdin.fileno(), 4096)
|
|
37
|
+
if data:
|
|
38
|
+
os.write(master, data)
|
|
39
|
+
else:
|
|
40
|
+
stdin_open = False
|
|
41
|
+
if process.poll() is not None and not ready:
|
|
42
|
+
break
|
|
43
|
+
except KeyboardInterrupt:
|
|
44
|
+
interrupted = True
|
|
45
|
+
finally:
|
|
46
|
+
if process.poll() is None:
|
|
47
|
+
try:
|
|
48
|
+
process.wait(timeout=2 if not interrupted else 0.1)
|
|
49
|
+
except subprocess.TimeoutExpired:
|
|
50
|
+
process.terminate()
|
|
51
|
+
try:
|
|
52
|
+
process.wait(timeout=2)
|
|
53
|
+
except subprocess.TimeoutExpired:
|
|
54
|
+
process.kill()
|
|
55
|
+
process.wait()
|
|
56
|
+
os.close(master)
|
|
57
|
+
sys.exit(1 if interrupted else (process.returncode or 0))
|
package/src/read-json.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
async function readJson(response,maxBytes=131072){
|
|
3
|
+
const declared=Number(response.headers.get('content-length')||0);if(declared>maxBytes)throw new Error('response-too-large');
|
|
4
|
+
const reader=response.body?.getReader();if(!reader)throw new Error('response-unavailable');let size=0;const chunks=[];
|
|
5
|
+
for(;;){const {value,done}=await reader.read();if(done)break;size+=value.byteLength;if(size>maxBytes){await reader.cancel();throw new Error('response-too-large');}chunks.push(Buffer.from(value));}
|
|
6
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
7
|
+
}
|
|
8
|
+
module.exports={readJson};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { brokerArgsForClient } = require('../desktop/client-identity');
|
|
4
|
+
const { SLAUNT_SERVER_NAME } = require('../mcp-identity');
|
|
5
|
+
const { catalog } = require('../discovery/catalog');
|
|
6
|
+
const { installMcpConfigForClient } = require('./mcp-config-installer');
|
|
7
|
+
const { installHooksForDetectedClients } = require('./hook-installer');
|
|
8
|
+
|
|
9
|
+
const SUPPORTED_CLIENTS = new Set(['claude-code', 'codex-cli']);
|
|
10
|
+
const SUCCESSFUL_MCP_STATES = new Set(['installed', 'unchanged']);
|
|
11
|
+
|
|
12
|
+
function installSelectedClients({
|
|
13
|
+
clients = [],
|
|
14
|
+
selectedClientIds = [],
|
|
15
|
+
homeDir,
|
|
16
|
+
broker,
|
|
17
|
+
fileSystem
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (!Array.isArray(selectedClientIds) || selectedClientIds.length > SUPPORTED_CLIENTS.size) {
|
|
20
|
+
return { results: [], summary: { failed: 1, installed: 0, unchanged: 0 } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const uniqueIds = [...new Set(selectedClientIds)];
|
|
24
|
+
if (uniqueIds.some((id) => typeof id !== 'string' || !SUPPORTED_CLIENTS.has(id))) {
|
|
25
|
+
return { results: [], summary: { failed: 1, installed: 0, unchanged: 0 } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const clientById = new Map();
|
|
29
|
+
for (const client of clients) {
|
|
30
|
+
if (SUPPORTED_CLIENTS.has(client?.id) && !clientById.has(client.id)) clientById.set(client.id, client);
|
|
31
|
+
}
|
|
32
|
+
const resultById = new Map();
|
|
33
|
+
const hookClients = [];
|
|
34
|
+
|
|
35
|
+
for (const id of uniqueIds) {
|
|
36
|
+
const client = clientById.get(id);
|
|
37
|
+
if (!client) {
|
|
38
|
+
resultById.set(id, {
|
|
39
|
+
id,
|
|
40
|
+
name: catalog.find((entry) => entry.id === id)?.name || id,
|
|
41
|
+
status: 'not-detected',
|
|
42
|
+
mcp: { status: 'not-installed', reason: 'client-not-detected' },
|
|
43
|
+
hooks: { status: 'not-installed', reason: 'client-not-detected' }
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const clientBroker = { ...broker, args: brokerArgsForClient(broker, id) };
|
|
49
|
+
const mcp = installMcpConfigForClient({ clientId: id, homeDir, broker: clientBroker });
|
|
50
|
+
const result = {
|
|
51
|
+
id,
|
|
52
|
+
name: client.name,
|
|
53
|
+
status: SUCCESSFUL_MCP_STATES.has(mcp.status) ? 'installed' : mcp.status,
|
|
54
|
+
mcp,
|
|
55
|
+
hooks: { status: 'not-installed', reason: 'mcp-configuration-required' }
|
|
56
|
+
};
|
|
57
|
+
resultById.set(id, result);
|
|
58
|
+
if (SUCCESSFUL_MCP_STATES.has(mcp.status)) hookClients.push(client);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (hookClients.length > 0) {
|
|
62
|
+
const hooks = installHooksForDetectedClients({
|
|
63
|
+
clients: hookClients,
|
|
64
|
+
selectedClientIds: hookClients.map((client) => client.id),
|
|
65
|
+
homeDir,
|
|
66
|
+
broker,
|
|
67
|
+
...(fileSystem ? { fileSystem } : {})
|
|
68
|
+
});
|
|
69
|
+
for (const result of hooks.results) {
|
|
70
|
+
const combined = resultById.get(result.id);
|
|
71
|
+
if (!combined) continue;
|
|
72
|
+
combined.hooks = { status: result.status, reason: result.reason };
|
|
73
|
+
if (result.status === 'error') combined.status = 'partial';
|
|
74
|
+
if (result.status === 'partial' && combined.status === 'installed') combined.status = 'partial';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results = uniqueIds.map((id) => resultById.get(id));
|
|
79
|
+
return {
|
|
80
|
+
serverName: SLAUNT_SERVER_NAME,
|
|
81
|
+
results,
|
|
82
|
+
summary: {
|
|
83
|
+
selected: results.length,
|
|
84
|
+
installed: results.filter((result) => result.status === 'installed').length,
|
|
85
|
+
partial: results.filter((result) => result.status === 'partial').length,
|
|
86
|
+
conflicts: results.filter((result) => result.status === 'conflict').length,
|
|
87
|
+
notDetected: results.filter((result) => result.status === 'not-detected').length,
|
|
88
|
+
failed: results.filter((result) => result.status === 'error').length
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { SUPPORTED_CLIENTS, installSelectedClients };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Converts guard process failures into a Codex PreToolUse denial. This program
|
|
4
|
+
// cannot control Codex behavior if the supervisor itself fails to start.
|
|
5
|
+
const {spawn}=require('node:child_process');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const MAX_INPUT=1024*1024, MAX_OUTPUT=64*1024, DEADLINE_MS=8000;
|
|
8
|
+
const denial=JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:'Slaunt tool authorization is unavailable.'}})+'\n';
|
|
9
|
+
let finished=false, child, input=[], inputBytes=0, output=[], outputBytes=0;
|
|
10
|
+
function finish(value=denial){if(finished)return;finished=true;clearTimeout(timer);if(child&&!child.killed)child.kill('SIGKILL');process.stdin.pause();process.stdout.write(value,()=>{process.exitCode=0;});}
|
|
11
|
+
const timer=setTimeout(()=>finish(),DEADLINE_MS);
|
|
12
|
+
const executable=process.argv[2];
|
|
13
|
+
if(process.argv.length!==3 || !executable || !path.isAbsolute(executable) || /[\x00-\x1f\x7f]/.test(executable)) finish();
|
|
14
|
+
else {
|
|
15
|
+
process.stdin.on('error',()=>finish());
|
|
16
|
+
process.stdin.on('data',chunk=>{inputBytes+=chunk.length;if(inputBytes>MAX_INPUT)return finish();input.push(chunk);});
|
|
17
|
+
process.stdin.on('end',()=>{
|
|
18
|
+
if(finished)return;
|
|
19
|
+
try {child=spawn(executable,[],{stdio:['pipe','pipe','ignore'],env:{...process.env,NODE_OPTIONS:'',NODE_PATH:''}});}catch{return finish();}
|
|
20
|
+
child.on('error',()=>finish());child.stdin.on('error',()=>finish());
|
|
21
|
+
child.stdout.on('data',chunk=>{outputBytes+=chunk.length;if(outputBytes>MAX_OUTPUT)return finish();output.push(chunk);});
|
|
22
|
+
child.stdout.on('error',()=>finish());
|
|
23
|
+
child.on('close',code=>{
|
|
24
|
+
if(finished)return;if(code!==0)return finish();
|
|
25
|
+
try {
|
|
26
|
+
const value=JSON.parse(Buffer.concat(output).toString('utf8'));
|
|
27
|
+
const hook=value?.hookSpecificOutput;
|
|
28
|
+
if(!hook||hook.hookEventName!=='PreToolUse'||!['deny',undefined].includes(hook.permissionDecision)||Object.keys(value).some(k=>k!=='hookSpecificOutput')||Object.keys(hook).some(k=>!['hookEventName','permissionDecision','permissionDecisionReason'].includes(k))|| (hook.permissionDecision==='deny'&&typeof hook.permissionDecisionReason!=='string')) return finish();
|
|
29
|
+
finish(JSON.stringify(value)+'\n');
|
|
30
|
+
}catch{finish();}
|
|
31
|
+
});
|
|
32
|
+
child.stdin.end(Buffer.concat(input));
|
|
33
|
+
});
|
|
34
|
+
}
|